From 1c8edbc819da939aaafdb43c9726e911f0d00f09 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sat, 1 Apr 2017 12:59:29 +0200 Subject: [PATCH 01/37] Switch to AVA for tests Has nice watch mode and output diffs --- package.json | 12 ++++-- test/common/jsurlTest.js | 83 ---------------------------------------- test/index.js | 25 ------------ test/jsurl.js | 83 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 112 deletions(-) delete mode 100644 test/common/jsurlTest.js delete mode 100644 test/index.js create mode 100644 test/jsurl.js diff --git a/package.json b/package.json index f8f9cd7..e3707e0 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,15 @@ "url": "git://github.com/Sage/jsurl.git" }, "devDependencies": { - "qunit": "^0.7.7" + "ava": "^0.18.2" + }, + "directories": { + "lib": "./lib" }, - "directories": {"lib": "./lib" }, "scripts": { - "test": "node test" + "test": "ava", + "test-watch": "ava --watch" }, - "main": "index.js" + "main": "index.js", + "dependencies": {} } diff --git a/test/common/jsurlTest.js b/test/common/jsurlTest.js deleted file mode 100644 index 1e03c80..0000000 --- a/test/common/jsurlTest.js +++ /dev/null @@ -1,83 +0,0 @@ -"use strict"; -QUnit.module(module.id); - -var JSURL = require("../.."); -var undefined; - -function t(v, r) { - strictEqual(JSURL.stringify(v), r, "stringify " + (typeof v !== 'object' ? v : JSON.stringify(v))); - strictEqual(JSURL.stringify(JSURL.parse(JSURL.stringify(v))), r, "roundtrip " + (typeof v !== 'object' ? v : JSON.stringify(v))); -} - -test('basic values', 26, function() { - t(undefined, undefined); - t(function() { - foo(); - }, undefined); - t(null, "~null"); - t(false, "~false"); - t(true, "~true"); - t(0, "~0"); - t(1, "~1"); - t(-1.5, "~-1.5"); - t("hello world\u203c", "~'hello*20world**203c"); - 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"); - // JSON.stringify converts special numeric values to null - t(NaN, "~null"); - t(Infinity, "~null"); - t(-Infinity, "~null"); -}); -test('arrays', 4, function() { - t([], "~(~)"); - t([undefined, function() { - foo(); - }, - null, false, 0, "hello world\u203c"], "~(~null~null~null~false~0~'hello*20world**203c)"); -}); -test('objects', 4, function() { - t({}, "~()"); - t({ - a: undefined, - b: function() { - foo(); - }, - c: null, - d: false, - e: 0, - f: "hello world\u203c" - }, "~(c~null~d~false~e~0~f~'hello*20world**203c)"); -}); -test('mix', 2, function() { - t({ - a: [ - [1, 2], - [], {}], - b: [], - c: { - d: "hello", - e: {}, - f: [] - } - }, "~(a~(~(~1~2)~(~)~())~b~(~)~c~(d~'hello~e~()~f~(~)))"); -}); - -test('percent-escaped single quotes', 1, function() { - deepEqual(JSURL.parse("~(a~%27hello~b~%27world)"), { - a: 'hello', - b: 'world' - }); -}); - -test('percent-escaped percent-escaped single quotes', 1, function() { - deepEqual(JSURL.parse("~(a~%2527hello~b~%2525252527world)"), { - a: 'hello', - b: 'world' - }); -}); - -test('tryParse', 4, function() { - strictEqual(JSURL.tryParse("~null"), null); - strictEqual(JSURL.tryParse("~1", 2), 1); - strictEqual(JSURL.tryParse("1"), undefined); - strictEqual(JSURL.tryParse("1", 0), 0); -}); diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 97e5741..0000000 --- a/test/index.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; - -var fs = require('fs'); -var fsp = require('path'); - -var root = fsp.join(__dirname, 'common'); -var tests = fs.readdirSync(root).filter(function(file) { - return /\.js$/.test(file); -}).map(function(file) { - return fsp.join(root, file); -}); - -var testrunner = require("qunit"); - -process.on('uncaughtException', function(err) { - console.error(err.stack); - process.exit(1); -}); - -testrunner.run({ - code: '', - tests: tests, -}, function(err) { - if (err) throw err; -}); diff --git a/test/jsurl.js b/test/jsurl.js new file mode 100644 index 0000000..6f986e3 --- /dev/null +++ b/test/jsurl.js @@ -0,0 +1,83 @@ +import test from 'ava' +import JSURL from '../lib/jsurl' + +// test macro, both directions +const cmp = (t, v, s) => { + // regular + t.is(JSURL.stringify(v), s) + // roundtrip + t.is(JSURL.stringify(JSURL.parse(s)), s) +} +cmp.title = (title, v, s) => `${title} ${s}` + +// basic values +test(cmp, undefined, undefined) +test(cmp, function () { + foo() +}, undefined) +test(cmp, null, '~null') +test(cmp, false, '~false') +test(cmp, true, '~true') +test(cmp, 0, '~0') +test(cmp, 1, '~1') +test(cmp, -1.5, '~-1.5') +test(cmp, 'hello world\u203c', "~'hello*20world**203c") +test(cmp, ' !"#$%&\'()*+,-./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") +// JSON.stringify converts special numeric values to null +test(cmp, NaN, '~null') +test(cmp, Infinity, '~null') +test(cmp, -Infinity, '~null') + +// arrays +test(cmp, [], '~(~)') +test(cmp, [undefined, function () { + foo() +}, + null, false, 0, 'hello world\u203c'], "~(~null~null~null~false~0~'hello*20world**203c)") + +// objects +test(cmp, {}, '~()') +test(cmp, { + a: undefined, + b: function () { + foo() + }, + c: null, + d: false, + e: 0, + f: 'hello world\u203c' +}, "~(c~null~d~false~e~0~f~'hello*20world**203c)") + +// mix +test(cmp, { + a: [ + [1, 2], + [], {}], + b: [], + c: { + d: 'hello', + e: {}, + f: [] + } +}, "~(a~(~(~1~2)~(~)~())~b~(~)~c~(d~'hello~e~()~f~(~)))") + +test('percent-escaped single quotes', t => { + t.deepEqual(JSURL.parse('~(a~%27hello~b~%27world)'), { + a: 'hello', + b: 'world' + }) +}) + +test('percent-escaped percent-escaped single quotes', t => { + t.deepEqual(JSURL.parse('~(a~%2527hello~b~%2525252527world)'), { + a: 'hello', + b: 'world' + }) +}) + +test('tryParse', t => { + t.is(JSURL.tryParse('~null'), null) + t.is(JSURL.tryParse('~1', 2), 1) + t.is(JSURL.tryParse('1'), undefined) + t.is(JSURL.tryParse('1', 0), 0) +}) From 4ffcdea624eb29070bd6c44510e438b46799e986 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sat, 1 Apr 2017 13:11:08 +0200 Subject: [PATCH 02/37] JSURL2 * shorter encoding * uses only unreserved symbols, plus '*' * fast encode/decode, uses more native functions * does not encode against URI encoding, but is immune on decode * supports Date objects --- lib/jsurl2.js | 192 +++++++++++++++++++++++++++++++++++++++++++++++++ test/jsurl2.js | 95 ++++++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 lib/jsurl2.js create mode 100644 test/jsurl2.js diff --git a/lib/jsurl2.js b/lib/jsurl2.js new file mode 100644 index 0000000..2cb97ec --- /dev/null +++ b/lib/jsurl2.js @@ -0,0 +1,192 @@ +// TODO custom objects, support Set, Map etc +// TODO custom dictionary +;(function (exports) { + 'use strict' + var stringRE = /^[a-zA-Z]/ + var numRE = /^\d/ + var TRUE = '-T' + var FALSE = '-F' + var NULL = '-N' + var UNDEF = '-U' + + var dict = { + T: true, + F: false, + N: null, + U: undefined, + } + + var fromEscape = { + '*': '*', + '_': '_', + '-': '~', + '.': '%', + 'S': '$', + 'P': '+', + '"': "'" + } + var toEscape = { + '*': '*', + '_': '_', + '~': '-', + '%': '.', + '$': 'S', + '+': 'P', + "'": '"' + } + function origChar (s) { + if (s === '_') { + return ' ' + } + var c = fromEscape[s.charAt(1)] + if (!c) { + throw new Error('Illegal escape code', s) + } + return c + } + function escCode (c) { + if (c === ' ') { + return '_' + } + return '*' + toEscape[c] + } + var escapeRE = /(_|\*.)/g + function unescape (s) { + return s.replace(escapeRE, origChar) + } + var replaceRE = /([*_~%$+' ])/g + function escape (s) { + return s.replace(replaceRE, escCode) + } + function eat (a) { + return a.splice(0, 1)[0] + } + + function decode (a) { + if (a.length === 0) {return } + var out, k, t + var t = eat(a) + if (!t) {return true} + var c = t.charAt(0) + if (c === '_') { + out = {} + k = unescape(t.slice(1)) + while (k) { + if (!a.length) { + throw new Error('expected value after object key') + } + out[k] = decode(a) + k = false + if (a.length) { + t = eat(a) + if (t) { + k = unescape(t) + } + } + } + } else if (c === '.') { + out = [] + a.unshift(t.slice(1)) + while (a[0]) { + out.push(decode(a)) + } + a.splice(0, 1) + } else if (c === '-') { + if (/^\d/.test(t.charAt(1))) { + out = Number(t) + } else { + k = unescape(t.slice(1)) + if (k.charAt(0) === 'D') { + out = new Date(k.slice(1)) + } else if (k in dict) { + out = dict[k] + } else { + throw new Error('Unknown dict reference', k) + } + } + } else if (numRE.test(c)) { + out = Number(t) + } else if (stringRE.test(c)) { + out = unescape(t) + } else if (c === '*') { + out = unescape(t.slice(1)) + } else { + throw new Error('Cannot decode part ' + [t].concat(a).join('~')) + } + return out + } + + function encode (v) { + var t, a, out, T = typeof v + + if (T === 'number') { + out = isFinite(v) ? v : NULL + } else if (T === 'boolean') { + out = v ? '' : FALSE + } else if (T === 'string') { + t = escape(v) + if (stringRE.test(t)) { + out = t + } else { + out = '*' + t + } + } else if (T === 'object') { + if (!v) { + out = NULL + } else if (v instanceof Date) { + out = '-D' + v.toJSON().replace('T00:00:00.000Z', '') + } else if (Array.isArray(v)) { + a = [] + for (var i = 0; i < v.length; i++) { + t = encode(v[i]) + // Special case: only use full -T~ in arrays + a[i] = (t === '~' ? TRUE + '~' : t) || NULL + } + + out = '.' + a.join('') + } else { + a = [] + for (var key in v) { + if (v.hasOwnProperty(key)) { + var val = encode(v[key]) + + // skip undefined and functions + if (val !== UNDEF + '~') { + a.push(escape(key) + '~', val) + } + } + } + + out = '_' + a.join('') + } + } else { + // function, undefined + out = UNDEF + } + return out + '~' + } + + exports.stringify = function (v) { + return encode(v).replace(/~+$/, '~') + } + + exports.parse = function (s) { + if (!s) return s + while (s.indexOf('%') !== -1) { + s = decodeURIComponent(s) + } + if (s.slice(-1) !== '~') { + throw new Error('not a JSURL2 string') + } + var parts = s.slice(0, -1).split('~') + return decode(parts) + } + + exports.tryParse = function (s, def) { + try { + return exports.parse(s) + } catch (ex) { + return def + } + } +})(typeof exports !== 'undefined' ? exports : (window.JSURL2 = window.JSURL2 || {})) diff --git a/test/jsurl2.js b/test/jsurl2.js new file mode 100644 index 0000000..172d313 --- /dev/null +++ b/test/jsurl2.js @@ -0,0 +1,95 @@ +import test from 'ava' +import JSURL from '../lib/jsurl2' + +// test macro, both directions +const cmp = (t, v, s) => { + // regular + t.is(JSURL.stringify(v), s) + // roundtrip + t.is(JSURL.stringify(JSURL.parse(s)), s) +} +cmp.title = (title, v, s) => `${title} ${s}` + +// basic values +test(cmp, undefined, '-U~') +test(cmp, function () { foo(); }, '-U~') +test(cmp, null, '-N~') +test(cmp, false, '-F~') +test(cmp, true, '~') +test(cmp, 0, '0~') +test(cmp, 1, '1~') +test(cmp, -1.5, '-1.5~') +test(cmp, 'hello world\u203c', 'hello_world\u203c~') +test(cmp, ' !"#$%&\'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~', "*_!\"#*S*.&*\"()***P,-./09:;<=>?@AZ[\\]^*_`az{|}*-~") +// JSON.stringify converts special numeric values to null +test(cmp, NaN, '-N~') +test(cmp, Infinity, '-N~') +test(cmp, -Infinity, '-N~') +test(cmp, new Date(1456898746898), '-D2016-03-02T06:05:46.898Z~') +test(cmp, new Date('2017-04-01'), '-D2017-04-01~') + +// arrays +test(cmp, [], '.~') +test(cmp, + [ + undefined, function () { + foo() + }, + null, false, 0, 'hello world\u203c' + ], + ".-U~-U~-N~-F~0~hello_world\u203c~" +) + +// objects +test(cmp, {}, '_~') +test(cmp, { + a: undefined, + b: function () { + foo() + }, + c: null, + d: false, + t: true, + e: 0, + f: 'hello world\u203c' +}, "_c~-N~d~-F~t~~e~0~f~hello_world\u203c~") + +// mix +test(cmp, { + a: [ + [1, 2], + [], + false, + {}, + true, + ], + b: [], + c: { + d: 'hello', + e: {}, + f: [], + g: true, + n: null, + } +}, '_a~..1~2~~.~-F~_~-T~~b~.~c~_d~hello~e~_~f~.~g~~n~-N~') + +test('percent-escaped single quotes', t => { + t.deepEqual(JSURL.parse('_a~*%27hello~b~*%27world~'), { + a: '\'hello', + b: '\'world', + }) +}) + +test('percent-escaped percent-escaped single quotes', t => { + t.deepEqual(JSURL.parse('_a~*%2527hello~b~*%2525252527world~'), { + a: '\'hello', + b: '\'world' + }) +}) + +test('tryParse', t => { + t.is(JSURL.tryParse('-N~'), null) + t.is(JSURL.tryParse('1~', 2), 1) + t.is(JSURL.tryParse('1'), undefined) + t.is(JSURL.tryParse('1', 0), 0) +}) From 15cdfea0e98587ccc0ac16050c7e1682bc4f7606 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 2 Apr 2017 10:43:08 +0200 Subject: [PATCH 03/37] v2 rules update * prefix _ instead of - for special constants * () for objects * !~ for arrays --- lib/jsurl2.js | 55 +++++++++++++++++++++++++------------------------- test/jsurl2.js | 54 ++++++++++++++++++++++++------------------------- 2 files changed, 54 insertions(+), 55 deletions(-) diff --git a/lib/jsurl2.js b/lib/jsurl2.js index 2cb97ec..e45434f 100644 --- a/lib/jsurl2.js +++ b/lib/jsurl2.js @@ -3,11 +3,11 @@ ;(function (exports) { 'use strict' var stringRE = /^[a-zA-Z]/ - var numRE = /^\d/ - var TRUE = '-T' - var FALSE = '-F' - var NULL = '-N' - var UNDEF = '-U' + var numRE = /^[\d-]/ + var TRUE = '_T' + var FALSE = '_F' + var NULL = '_N' + var UNDEF = '_U' var dict = { T: true, @@ -63,15 +63,18 @@ } function decode (a) { - if (a.length === 0) {return } + if (a.length === 0) { + throw new Error('got empty array?') + } var out, k, t var t = eat(a) if (!t) {return true} var c = t.charAt(0) - if (c === '_') { + if (c === '(') { out = {} - k = unescape(t.slice(1)) - while (k) { + t = t.slice(1) + while (t && t.charAt(0) !== ')') { + k = unescape(t) if (!a.length) { throw new Error('expected value after object key') } @@ -79,30 +82,26 @@ k = false if (a.length) { t = eat(a) - if (t) { - k = unescape(t) - } } } - } else if (c === '.') { + if (t.charAt(0) === ')') { + a.unshift(t.slice(1)) + } + } else if (c === '!') { out = [] a.unshift(t.slice(1)) while (a[0]) { out.push(decode(a)) } a.splice(0, 1) - } else if (c === '-') { - if (/^\d/.test(t.charAt(1))) { - out = Number(t) + } else if (c === '_') { + k = unescape(t.slice(1)) + if (k.charAt(0) === 'D') { + out = new Date(k.slice(1)) + } else if (k in dict) { + out = dict[k] } else { - k = unescape(t.slice(1)) - if (k.charAt(0) === 'D') { - out = new Date(k.slice(1)) - } else if (k in dict) { - out = dict[k] - } else { - throw new Error('Unknown dict reference', k) - } + throw new Error('Unknown dict reference', k) } } else if (numRE.test(c)) { out = Number(t) @@ -134,7 +133,7 @@ if (!v) { out = NULL } else if (v instanceof Date) { - out = '-D' + v.toJSON().replace('T00:00:00.000Z', '') + out = '_D' + v.toJSON().replace('T00:00:00.000Z', '') } else if (Array.isArray(v)) { a = [] for (var i = 0; i < v.length; i++) { @@ -143,7 +142,7 @@ a[i] = (t === '~' ? TRUE + '~' : t) || NULL } - out = '.' + a.join('') + out = '!' + a.join('') } else { a = [] for (var key in v) { @@ -157,7 +156,7 @@ } } - out = '_' + a.join('') + return '(' + a.join('') + ')' } } else { // function, undefined @@ -167,7 +166,7 @@ } exports.stringify = function (v) { - return encode(v).replace(/~+$/, '~') + return encode(v).replace(/~*$/, '~') } exports.parse = function (s) { diff --git a/test/jsurl2.js b/test/jsurl2.js index 172d313..5558386 100644 --- a/test/jsurl2.js +++ b/test/jsurl2.js @@ -11,25 +11,25 @@ const cmp = (t, v, s) => { cmp.title = (title, v, s) => `${title} ${s}` // basic values -test(cmp, undefined, '-U~') -test(cmp, function () { foo(); }, '-U~') -test(cmp, null, '-N~') -test(cmp, false, '-F~') +test(cmp, undefined, '_U~') +test(cmp, function () { foo(); }, '_U~') +test(cmp, null, '_N~') +test(cmp, false, '_F~') test(cmp, true, '~') test(cmp, 0, '0~') test(cmp, 1, '1~') test(cmp, -1.5, '-1.5~') test(cmp, 'hello world\u203c', 'hello_world\u203c~') -test(cmp, ' !"#$%&\'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~', "*_!\"#*S*.&*\"()***P,-./09:;<=>?@AZ[\\]^*_`az{|}*-~") +test(cmp, ' !"#$%&\'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~', '*_!"#*S*.&*"()***P,-./09:;<=>?@AZ[\\]^*_`az{|}*-~') // JSON.stringify converts special numeric values to null -test(cmp, NaN, '-N~') -test(cmp, Infinity, '-N~') -test(cmp, -Infinity, '-N~') -test(cmp, new Date(1456898746898), '-D2016-03-02T06:05:46.898Z~') -test(cmp, new Date('2017-04-01'), '-D2017-04-01~') +test(cmp, NaN, '_N~') +test(cmp, Infinity, '_N~') +test(cmp, -Infinity, '_N~') +test(cmp, new Date(1456898746898), '_D2016-03-02T06:05:46.898Z~') +test(cmp, new Date('2017-04-01'), '_D2017-04-01~') // arrays -test(cmp, [], '.~') +test(cmp, [], '!~') test(cmp, [ undefined, function () { @@ -37,11 +37,11 @@ test(cmp, }, null, false, 0, 'hello world\u203c' ], - ".-U~-U~-N~-F~0~hello_world\u203c~" + '!_U~_U~_N~_F~0~hello_world\u203c~' ) // objects -test(cmp, {}, '_~') +test(cmp, {}, '()~') test(cmp, { a: undefined, b: function () { @@ -51,8 +51,8 @@ test(cmp, { d: false, t: true, e: 0, - f: 'hello world\u203c' -}, "_c~-N~d~-F~t~~e~0~f~hello_world\u203c~") + f: 'hello (world)\u203c' +}, '(c~_N~d~_F~t~~e~0~f~hello_(world)\u203c~)~') // mix test(cmp, { @@ -60,35 +60,35 @@ test(cmp, { [1, 2], [], false, - {}, true, + {}, ], - b: [], c: { d: 'hello', e: {}, f: [], g: true, - n: null, - } -}, '_a~..1~2~~.~-F~_~-T~~b~.~c~_d~hello~e~_~f~.~g~~n~-N~') + n: null + }, + b: [], +}, '(a~!!1~2~~!~_F~_T~()~c~(d~hello~e~()f~!~g~~n~_N~)b~!~)~') test('percent-escaped single quotes', t => { - t.deepEqual(JSURL.parse('_a~*%27hello~b~*%27world~'), { - a: '\'hello', - b: '\'world', + t.deepEqual(JSURL.parse('(a~*%27hello~b~*%27world~)~'), { + a: "'hello", + b: "'world" }) }) test('percent-escaped percent-escaped single quotes', t => { - t.deepEqual(JSURL.parse('_a~*%2527hello~b~*%2525252527world~'), { - a: '\'hello', - b: '\'world' + t.deepEqual(JSURL.parse('(a~*%2527hello~b~*%2525252527world~)~'), { + a: "'hello", + b: "'world" }) }) test('tryParse', t => { - t.is(JSURL.tryParse('-N~'), null) + t.is(JSURL.tryParse('_N~'), null) t.is(JSURL.tryParse('1~', 2), 1) t.is(JSURL.tryParse('1'), undefined) t.is(JSURL.tryParse('1', 0), 0) From fd512d6e98c88d34aabfe75558391dc9af317566 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 2 Apr 2017 10:45:48 +0200 Subject: [PATCH 04/37] Make sure individual imports work in ES6 --- test/jsurl.js | 18 +++++++++--------- test/jsurl2.js | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/test/jsurl.js b/test/jsurl.js index 6f986e3..e36510f 100644 --- a/test/jsurl.js +++ b/test/jsurl.js @@ -1,12 +1,12 @@ import test from 'ava' -import JSURL from '../lib/jsurl' +import {stringify, parse, tryParse} from '../lib/jsurl' // test macro, both directions const cmp = (t, v, s) => { // regular - t.is(JSURL.stringify(v), s) + t.is(stringify(v), s) // roundtrip - t.is(JSURL.stringify(JSURL.parse(s)), s) + t.is(stringify(parse(s)), s) } cmp.title = (title, v, s) => `${title} ${s}` @@ -62,22 +62,22 @@ test(cmp, { }, "~(a~(~(~1~2)~(~)~())~b~(~)~c~(d~'hello~e~()~f~(~)))") test('percent-escaped single quotes', t => { - t.deepEqual(JSURL.parse('~(a~%27hello~b~%27world)'), { + t.deepEqual(parse('~(a~%27hello~b~%27world)'), { a: 'hello', b: 'world' }) }) test('percent-escaped percent-escaped single quotes', t => { - t.deepEqual(JSURL.parse('~(a~%2527hello~b~%2525252527world)'), { + t.deepEqual(parse('~(a~%2527hello~b~%2525252527world)'), { a: 'hello', b: 'world' }) }) test('tryParse', t => { - t.is(JSURL.tryParse('~null'), null) - t.is(JSURL.tryParse('~1', 2), 1) - t.is(JSURL.tryParse('1'), undefined) - t.is(JSURL.tryParse('1', 0), 0) + t.is(tryParse('~null'), null) + t.is(tryParse('~1', 2), 1) + t.is(tryParse('1'), undefined) + t.is(tryParse('1', 0), 0) }) diff --git a/test/jsurl2.js b/test/jsurl2.js index 5558386..c041036 100644 --- a/test/jsurl2.js +++ b/test/jsurl2.js @@ -1,12 +1,12 @@ import test from 'ava' -import JSURL from '../lib/jsurl2' +import {stringify, parse, tryParse} from '../lib/jsurl2' // test macro, both directions const cmp = (t, v, s) => { // regular - t.is(JSURL.stringify(v), s) + t.is(stringify(v), s) // roundtrip - t.is(JSURL.stringify(JSURL.parse(s)), s) + t.is(stringify(parse(s)), s) } cmp.title = (title, v, s) => `${title} ${s}` @@ -74,22 +74,22 @@ test(cmp, { }, '(a~!!1~2~~!~_F~_T~()~c~(d~hello~e~()f~!~g~~n~_N~)b~!~)~') test('percent-escaped single quotes', t => { - t.deepEqual(JSURL.parse('(a~*%27hello~b~*%27world~)~'), { + t.deepEqual(parse('(a~*%27hello~b~*%27world~)~'), { a: "'hello", b: "'world" }) }) test('percent-escaped percent-escaped single quotes', t => { - t.deepEqual(JSURL.parse('(a~*%2527hello~b~*%2525252527world~)~'), { + t.deepEqual(parse('(a~*%2527hello~b~*%2525252527world~)~'), { a: "'hello", b: "'world" }) }) test('tryParse', t => { - t.is(JSURL.tryParse('_N~'), null) - t.is(JSURL.tryParse('1~', 2), 1) - t.is(JSURL.tryParse('1'), undefined) - t.is(JSURL.tryParse('1', 0), 0) + t.is(tryParse('_N~'), null) + t.is(tryParse('1~', 2), 1) + t.is(tryParse('1'), undefined) + t.is(tryParse('1', 0), 0) }) From b6ac1e243285ccf56495140be0425c472e901d90 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 2 Apr 2017 10:46:58 +0200 Subject: [PATCH 05/37] Poor man's performance "test" Very variable but gives an idea. v1 is twice as fast as v2 despite doing more work :( Needs profiling. --- test/jsurl.js | 24 ++++++++++++++++++++++++ test/jsurl2.js | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/test/jsurl.js b/test/jsurl.js index e36510f..f066e2f 100644 --- a/test/jsurl.js +++ b/test/jsurl.js @@ -81,3 +81,27 @@ test('tryParse', t => { t.is(tryParse('1'), undefined) t.is(tryParse('1', 0), 0) }) + +test('parse performance', t => { + const n = Date.now() + const s = '~(a~%2527hello~b~%2525252527world)' + const count = 10000 + for (let i = 0; i < count; i++) { + parse(s) + } + const ms = Date.now() - n + console.log(`${count} parsed in ${ms}ms, ${ms / count}ms/item`) + t.true(ms < 300) +}) + +test('stringify performance', t => { + const n = Date.now() + const v = { a: [ [1, 2], [], false, {}, true ], b: [], c: { d: 'hello', e: {}, f: [], g: true, n: null } } + const count = 10000 + for (let i = 0; i < count; i++) { + stringify(v) + } + const ms = Date.now() - n + console.log(`${count} stringified in ${ms}ms, ${ms / count}ms/item`) + t.true(ms < 300) +}) diff --git a/test/jsurl2.js b/test/jsurl2.js index c041036..cae7c6c 100644 --- a/test/jsurl2.js +++ b/test/jsurl2.js @@ -93,3 +93,27 @@ test('tryParse', t => { t.is(tryParse('1'), undefined) t.is(tryParse('1', 0), 0) }) + +test('parse performance', t => { + const n = Date.now() + const s = '(a~*%2527hello~b~*%2525252527world~)~' + const count = 10000 + for (let i = 0; i < count; i++) { + parse(s) + } + const ms = Date.now() - n + console.log(`v2: ${count} parsed in ${ms}ms, ${ms / count}ms/item`) + t.true(ms < 300) +}) + +test('stringify performance', t => { + const n = Date.now() + const v = { a: [ [1, 2], [], false, {}, true ], b: [], c: { d: 'hello', e: {}, f: [], g: true, n: null } } + const count = 10000 + for (let i = 0; i < count; i++) { + stringify(v) + } + const ms = Date.now() - n + console.log(`v2: ${count} stringified in ${ms}ms, ${ms / count}ms/item`) + t.true(ms < 300) +}) From 1b6af29f5a18982d6c0f9af8f793f78de44eed5e Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 2 Apr 2017 14:03:52 +0200 Subject: [PATCH 06/37] v2: make ~ on last element of object optional * not for final ~ representing `true` value * switch parser to using string indexes instead of splitting into array, so the end-of-object can be distinguished from ~ --- lib/jsurl2.js | 109 +++++++++++++++++++++++++++++++------------------ test/jsurl2.js | 20 +++++---- 2 files changed, 82 insertions(+), 47 deletions(-) diff --git a/lib/jsurl2.js b/lib/jsurl2.js index e45434f..26d1304 100644 --- a/lib/jsurl2.js +++ b/lib/jsurl2.js @@ -23,7 +23,9 @@ '.': '%', 'S': '$', 'P': '+', - '"': "'" + '"': "'", + 'C': '(', + 'D': ')', } var toEscape = { '*': '*', @@ -32,7 +34,9 @@ '%': '.', '$': 'S', '+': 'P', - "'": '"' + "'": '"', + '(': 'C', + ')': 'D', } function origChar (s) { if (s === '_') { @@ -54,48 +58,64 @@ function unescape (s) { return s.replace(escapeRE, origChar) } - var replaceRE = /([*_~%$+' ])/g + var replaceRE = /([*_~%$+'() ])/g function escape (s) { return s.replace(replaceRE, escCode) } function eat (a) { - return a.splice(0, 1)[0] + var j, c + for ( + var j = a.i; + j < a.l && (c = a.s.charAt(j), c !== '~' && c !== ')'); + j++ + ) {} + var w = a.s.slice(a.i, j) + if (c === '~') { + j++ + } + a.i = j + return w } - + function peek (a) { + return a.s.charAt(a.i) + } + function eatOne (a) { + a.i++ + } + var EOS = {} // unique symbol function decode (a) { - if (a.length === 0) { - throw new Error('got empty array?') - } var out, k, t - var t = eat(a) - if (!t) {return true} - var c = t.charAt(0) - if (c === '(') { + var c = peek(a) + if (!c) {return EOS} + if (c === '~') { + eatOne(a) + out = true + } else if (c === '(') { + eatOne(a) out = {} - t = t.slice(1) - while (t && t.charAt(0) !== ')') { - k = unescape(t) - if (!a.length) { + while (c = peek(a), c && c !== ')') { + k = unescape(eat(a)) + t = decode(a) + if (t === EOS) { throw new Error('expected value after object key') } - out[k] = decode(a) - k = false - if (a.length) { - t = eat(a) - } + out[k] = t } - if (t.charAt(0) === ')') { - a.unshift(t.slice(1)) + if (c === ')') { + eatOne(a) } } else if (c === '!') { + eatOne(a) out = [] - a.unshift(t.slice(1)) - while (a[0]) { + while (c = peek(a), c && c !== '~' && c !== ')') { out.push(decode(a)) } - a.splice(0, 1) + if (c === '~') { + eatOne(a) + } } else if (c === '_') { - k = unescape(t.slice(1)) + eatOne(a) + k = unescape(eat(a)) if (k.charAt(0) === 'D') { out = new Date(k.slice(1)) } else if (k in dict) { @@ -104,19 +124,21 @@ throw new Error('Unknown dict reference', k) } } else if (numRE.test(c)) { - out = Number(t) + out = Number(eat(a)) } else if (stringRE.test(c)) { - out = unescape(t) + out = unescape(eat(a)) } else if (c === '*') { - out = unescape(t.slice(1)) + eatOne(a) + out = unescape(eat(a)) } else { throw new Error('Cannot decode part ' + [t].concat(a).join('~')) } return out } + var endTildesRE = /~*$/ function encode (v) { - var t, a, out, T = typeof v + var t, a, out, T = typeof v, val if (T === 'number') { out = isFinite(v) ? v : NULL @@ -145,9 +167,10 @@ out = '!' + a.join('') } else { a = [] + val = undefined for (var key in v) { if (v.hasOwnProperty(key)) { - var val = encode(v[key]) + val = encode(v[key]) // skip undefined and functions if (val !== UNDEF + '~') { @@ -155,8 +178,12 @@ } } } + t = a.join('') + if (val !== true) { + t = t.replace(endTildesRE, '') + } - return '(' + a.join('') + ')' + return '(' + t + ')' } } else { // function, undefined @@ -166,19 +193,21 @@ } exports.stringify = function (v) { - return encode(v).replace(/~*$/, '~') + return encode(v).replace(endTildesRE, '~') } - exports.parse = function (s) { + exports.parse = function (s, options) { if (!s) return s - while (s.indexOf('%') !== -1) { - s = decodeURIComponent(s) + if (options && options.deURI) { + while (s.indexOf('%') !== -1) { + s = decodeURIComponent(s) + } } - if (s.slice(-1) !== '~') { + var l = s.length + if (s.charAt(l - 1) !== '~') { throw new Error('not a JSURL2 string') } - var parts = s.slice(0, -1).split('~') - return decode(parts) + return decode({s: s, i: 0, l: l}) } exports.tryParse = function (s, def) { diff --git a/test/jsurl2.js b/test/jsurl2.js index cae7c6c..1659e76 100644 --- a/test/jsurl2.js +++ b/test/jsurl2.js @@ -20,7 +20,10 @@ test(cmp, 0, '0~') test(cmp, 1, '1~') test(cmp, -1.5, '-1.5~') test(cmp, 'hello world\u203c', 'hello_world\u203c~') -test(cmp, ' !"#$%&\'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~', '*_!"#*S*.&*"()***P,-./09:;<=>?@AZ[\\]^*_`az{|}*-~') +test(cmp, + ' !"#$%&\'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~', + '*_!"#*S*.&*"*C*D***P,-./09:;<=>?@AZ[\\]^*_`az{|}*-~' +) // JSON.stringify converts special numeric values to null test(cmp, NaN, '_N~') test(cmp, Infinity, '_N~') @@ -52,8 +55,9 @@ test(cmp, { t: true, e: 0, f: 'hello (world)\u203c' -}, '(c~_N~d~_F~t~~e~0~f~hello_(world)\u203c~)~') - +}, '(c~_N~d~_F~t~~e~0~f~hello_*Cworld*D\u203c)~') +test(cmp, {"()": {}, c: {"~": "()"}}, '(*C*D~()c~(*-~**C*D))~') +test(cmp, {a: [[[1]]]}, '(a~!!!1)~') // mix test(cmp, { a: [ @@ -71,17 +75,18 @@ test(cmp, { n: null }, b: [], -}, '(a~!!1~2~~!~_F~_T~()~c~(d~hello~e~()f~!~g~~n~_N~)b~!~)~') +}, '(a~!!1~2~~!~_F~_T~()~c~(d~hello~e~()f~!~g~~n~_N)b~!)~') +test(cmp, [[{a: [{b: [[1]]}]}]], '!!(a~!(b~!!1))~') test('percent-escaped single quotes', t => { - t.deepEqual(parse('(a~*%27hello~b~*%27world~)~'), { + t.deepEqual(parse('(a~*%27hello~b~*%27world~)~', {deURI: true}), { a: "'hello", b: "'world" }) }) test('percent-escaped percent-escaped single quotes', t => { - t.deepEqual(parse('(a~*%2527hello~b~*%2525252527world~)~'), { + t.deepEqual(parse('(a~*%2527hello~b~*%2525252527world~)~', {deURI: true}), { a: "'hello", b: "'world" }) @@ -96,7 +101,8 @@ test('tryParse', t => { test('parse performance', t => { const n = Date.now() - const s = '(a~*%2527hello~b~*%2525252527world~)~' + const v = { a: [ [1, 2], [], false, {}, true ], b: [], c: { d: 'hello', e: {}, f: [], g: true, n: null } } + const s = stringify(v) const count = 10000 for (let i = 0; i < count; i++) { parse(s) From bea30da5fcfc32515cab85a905bc65ba13335e24 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 2 Apr 2017 14:05:30 +0200 Subject: [PATCH 07/37] Add performance.html to use chrome profiling tools v2 parse is faster than v1, but stringify is slower --- examples/performance.html | 50 +++++++++++++++++++++++++++++++++++++++ test/jsurl.js | 33 +++++++++++++------------- 2 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 examples/performance.html diff --git a/examples/performance.html b/examples/performance.html new file mode 100644 index 0000000..0f32cd5 --- /dev/null +++ b/examples/performance.html @@ -0,0 +1,50 @@ + +JSURL Performance + + + + +

Use the Chrome Devtools Timeline to profile page load. See JS console for timings.

\ No newline at end of file diff --git a/test/jsurl.js b/test/jsurl.js index f066e2f..b8555ff 100644 --- a/test/jsurl.js +++ b/test/jsurl.js @@ -83,25 +83,26 @@ test('tryParse', t => { }) test('parse performance', t => { - const n = Date.now() - const s = '~(a~%2527hello~b~%2525252527world)' + const n = Date.now() + const v = { a: [ [1, 2], [], false, {}, true ], b: [], c: { d: 'hello', e: {}, f: [], g: true, n: null } } + const s = stringify(v) const count = 10000 - for (let i = 0; i < count; i++) { - parse(s) - } - const ms = Date.now() - n - console.log(`${count} parsed in ${ms}ms, ${ms / count}ms/item`) - t.true(ms < 300) + for (let i = 0; i < count; i++) { + parse(s) + } + const ms = Date.now() - n + console.log(`${count} parsed in ${ms}ms, ${ms / count}ms/item`) + t.true(ms < 300) }) test('stringify performance', t => { - const n = Date.now() - const v = { a: [ [1, 2], [], false, {}, true ], b: [], c: { d: 'hello', e: {}, f: [], g: true, n: null } } + const n = Date.now() + const v = { a: [ [1, 2], [], false, {}, true ], b: [], c: { d: 'hello', e: {}, f: [], g: true, n: null } } const count = 10000 - for (let i = 0; i < count; i++) { - stringify(v) - } - const ms = Date.now() - n - console.log(`${count} stringified in ${ms}ms, ${ms / count}ms/item`) - t.true(ms < 300) + for (let i = 0; i < count; i++) { + stringify(v) + } + const ms = Date.now() - n + console.log(`${count} stringified in ${ms}ms, ${ms / count}ms/item`) + t.true(ms < 300) }) From ed7f6b646f11692107fabe976de794b59ac27516 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 2 Apr 2017 16:12:31 +0200 Subject: [PATCH 08/37] Add `short` option and fix `true` in objects --- lib/jsurl2.js | 33 +++++++++++--------- test/jsurl2.js | 82 +++++++++++++++++++++++++++++--------------------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/lib/jsurl2.js b/lib/jsurl2.js index 26d1304..963ad89 100644 --- a/lib/jsurl2.js +++ b/lib/jsurl2.js @@ -95,9 +95,11 @@ out = {} while (c = peek(a), c && c !== ')') { k = unescape(eat(a)) - t = decode(a) - if (t === EOS) { - throw new Error('expected value after object key') + c = peek(a) + if (c && c !== ')') { + t = decode(a) + } else { + t = true } out[k] = t } @@ -167,7 +169,6 @@ out = '!' + a.join('') } else { a = [] - val = undefined for (var key in v) { if (v.hasOwnProperty(key)) { val = encode(v[key]) @@ -178,10 +179,7 @@ } } } - t = a.join('') - if (val !== true) { - t = t.replace(endTildesRE, '') - } + t = a.join('').replace(endTildesRE, '') return '(' + t + ')' } @@ -192,22 +190,27 @@ return out + '~' } - exports.stringify = function (v) { - return encode(v).replace(endTildesRE, '~') + var allTerminatorsRE = /\)*~*$/ + exports.stringify = function (v, options) { + var r = encode(v) + if (options && options.short) { + return r.replace(allTerminatorsRE, '') + } + return r.replace(endTildesRE, '~') } exports.parse = function (s, options) { - if (!s) return s if (options && options.deURI) { while (s.indexOf('%') !== -1) { s = decodeURIComponent(s) } } var l = s.length - if (s.charAt(l - 1) !== '~') { - throw new Error('not a JSURL2 string') - } - return decode({s: s, i: 0, l: l}) + // if (s.charAt(l - 1) !== '~') { + // throw new Error('not a JSURL2 string') + // } + var r = decode({s: s, i: 0, l: l}) + return r === EOS ? true : r } exports.tryParse = function (s, def) { diff --git a/test/jsurl2.js b/test/jsurl2.js index 1659e76..dd04b49 100644 --- a/test/jsurl2.js +++ b/test/jsurl2.js @@ -2,37 +2,43 @@ import test from 'ava' import {stringify, parse, tryParse} from '../lib/jsurl2' // test macro, both directions -const cmp = (t, v, s) => { +const cmp = (t, v, s, short) => { // regular t.is(stringify(v), s) // roundtrip t.is(stringify(parse(s)), s) + // short + t.is(stringify(v, {short: true}), short) + t.is(stringify(parse(short), {short: true}), short) } cmp.title = (title, v, s) => `${title} ${s}` // basic values -test(cmp, undefined, '_U~') -test(cmp, function () { foo(); }, '_U~') -test(cmp, null, '_N~') -test(cmp, false, '_F~') -test(cmp, true, '~') -test(cmp, 0, '0~') -test(cmp, 1, '1~') -test(cmp, -1.5, '-1.5~') -test(cmp, 'hello world\u203c', 'hello_world\u203c~') +test(cmp, undefined, '_U~', '_U') +test(cmp, function () { foo(); }, '_U~', '_U') +test(cmp, null, '_N~', '_N') +test(cmp, false, '_F~', '_F') +test(cmp, true, '~', '') +test(cmp, 0, '0~', '0') +test(cmp, 1, '1~', '1') +test(cmp, -1.5, '-1.5~', '-1.5') +test(cmp, '', '*~', '*') +test(cmp, 'hello world\u203c', 'hello_world\u203c~', 'hello_world\u203c') test(cmp, ' !"#$%&\'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~', - '*_!"#*S*.&*"*C*D***P,-./09:;<=>?@AZ[\\]^*_`az{|}*-~' + '*_!"#*S*.&*"*C*D***P,-./09:;<=>?@AZ[\\]^*_`az{|}*-~', + '*_!"#*S*.&*"*C*D***P,-./09:;<=>?@AZ[\\]^*_`az{|}*-' ) // JSON.stringify converts special numeric values to null -test(cmp, NaN, '_N~') -test(cmp, Infinity, '_N~') -test(cmp, -Infinity, '_N~') -test(cmp, new Date(1456898746898), '_D2016-03-02T06:05:46.898Z~') -test(cmp, new Date('2017-04-01'), '_D2017-04-01~') +test(cmp, NaN, '_N~', '_N') +test(cmp, Infinity, '_N~', '_N') +test(cmp, -Infinity, '_N~', '_N') +test(cmp, new Date(1456898746898), '_D2016-03-02T06:05:46.898Z~', '_D2016-03-02T06:05:46.898Z') +test(cmp, new Date('2017-04-01'), '_D2017-04-01~', '_D2017-04-01') // arrays -test(cmp, [], '!~') +test(cmp, [], '!~', '!') +test(cmp, [true], '!_T~', '!_T') test(cmp, [ undefined, function () { @@ -40,24 +46,30 @@ test(cmp, }, null, false, 0, 'hello world\u203c' ], - '!_U~_U~_N~_F~0~hello_world\u203c~' + '!_U~_U~_N~_F~0~hello_world\u203c~', + '!_U~_U~_N~_F~0~hello_world\u203c' ) // objects -test(cmp, {}, '()~') -test(cmp, { - a: undefined, - b: function () { - foo() +test(cmp, {}, '()~', '(') +test(cmp, {a: true, b: true, c: true}, '(a~~b~~c)~', '(a~~b~~c') +test(cmp, + { + a: undefined, + b: function () { + foo() + }, + c: null, + d: false, + t: true, + e: 0, + f: 'hello (world)\u203c' }, - c: null, - d: false, - t: true, - e: 0, - f: 'hello (world)\u203c' -}, '(c~_N~d~_F~t~~e~0~f~hello_*Cworld*D\u203c)~') -test(cmp, {"()": {}, c: {"~": "()"}}, '(*C*D~()c~(*-~**C*D))~') -test(cmp, {a: [[[1]]]}, '(a~!!!1)~') + '(c~_N~d~_F~t~~e~0~f~hello_*Cworld*D\u203c)~', + '(c~_N~d~_F~t~~e~0~f~hello_*Cworld*D\u203c', +) +test(cmp, {"()": {}, c: {"~": "()"}}, '(*C*D~()c~(*-~**C*D))~', '(*C*D~()c~(*-~**C*D') +test(cmp, {a: [[[1]]]}, '(a~!!!1)~', '(a~!!!1') // mix test(cmp, { a: [ @@ -75,8 +87,8 @@ test(cmp, { n: null }, b: [], -}, '(a~!!1~2~~!~_F~_T~()~c~(d~hello~e~()f~!~g~~n~_N)b~!)~') -test(cmp, [[{a: [{b: [[1]]}]}]], '!!(a~!(b~!!1))~') +}, '(a~!!1~2~~!~_F~_T~()~c~(d~hello~e~()f~!~g~~n~_N)b~!)~', '(a~!!1~2~~!~_F~_T~()~c~(d~hello~e~()f~!~g~~n~_N)b~!') +test(cmp, [[{a: [{b: [[1]]}]}]], '!!(a~!(b~!!1))~', '!!(a~!(b~!!1') test('percent-escaped single quotes', t => { t.deepEqual(parse('(a~*%27hello~b~*%27world~)~', {deURI: true}), { @@ -95,8 +107,8 @@ test('percent-escaped percent-escaped single quotes', t => { test('tryParse', t => { t.is(tryParse('_N~'), null) t.is(tryParse('1~', 2), 1) - t.is(tryParse('1'), undefined) - t.is(tryParse('1', 0), 0) + t.is(tryParse('_'), undefined) + t.is(tryParse('_', 0), 0) }) test('parse performance', t => { From 5986de0242069abdd3c47ca808a138f56243d106 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sun, 2 Apr 2017 21:59:20 +0200 Subject: [PATCH 09/37] Performance improvements It stringifies slower than v1 and JSON, but it parses faster than v1 and JSON :) --- examples/performance.html | 19 +++++++++++++++++++ lib/jsurl2.js | 24 +++++++++++++----------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/examples/performance.html b/examples/performance.html index 0f32cd5..8bc9178 100644 --- a/examples/performance.html +++ b/examples/performance.html @@ -5,6 +5,23 @@ -

Use the Chrome Devtools Timeline to profile page load. See JS console for timings.

\ No newline at end of file +

Use the Chrome Devtools Timeline to profile page load. See JS console for timings.

From b9900c5a98517f86b55a71353fbac141fa58ca30 Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Mon, 16 Jul 2018 22:01:49 +0200 Subject: [PATCH 27/37] Make sure JSON can parse output only if same value --- __tests__/jsurl2.js | 29 ++++++++++++++++++++++++++--- lib/jsurl2.js | 7 ++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/__tests__/jsurl2.js b/__tests__/jsurl2.js index da4d699..8b0bbac 100644 --- a/__tests__/jsurl2.js +++ b/__tests__/jsurl2.js @@ -1,15 +1,29 @@ const {stringify, parse, tryParse} = require('..') +// It only produces JSON parseable values if they are the same +const isJsonOk = (v, str) => { + try { + const out = JSON.parse(str) + return v === out + } catch (err) { + return true + } +} // test macro, both directions const cmp = (v, s, short, rich) => { // regular - expect(stringify(v, {rich})).not.toMatch(/[%?#&=\n\r\0'<\\]/) - expect(stringify(v, {rich})).toBe(s) + const richStr = stringify(v, {rich}) + expect(richStr).not.toMatch(/[%?#&=\n\r\0'<\\]/) + expect(richStr).toBe(s) // roundtrip expect(stringify(parse(s), {rich})).toBe(s) // short - expect(stringify(v, {short: true, rich})).toBe(short) + const shortStr = stringify(v, {short: true, rich}) + expect(shortStr).toBe(short) expect(stringify(parse(short), {short: true, rich})).toBe(short) + // not JSON + expect(isJsonOk(v, richStr)).toBe(true) + expect(isJsonOk(v, shortStr)).toBe(true) } cmp.title = (title, v, s) => `${title} ${s}` @@ -191,3 +205,12 @@ test('.toJSON()', () => { } expect(stringify(o)).toBe('hi~') }) + +test('never JSON bareword', () => { + expect(stringify('true')).toEqual('true~') + expect(stringify('true', {short: true})).toEqual('*true') + expect(stringify('false')).toEqual('false~') + expect(stringify('false', {short: true})).toEqual('*false') + expect(stringify('null')).toEqual('null~') + expect(stringify('null', {short: true})).toEqual('*null') +}) diff --git a/lib/jsurl2.js b/lib/jsurl2.js index 6e70005..3723443 100644 --- a/lib/jsurl2.js +++ b/lib/jsurl2.js @@ -223,6 +223,8 @@ } } + const antiJSON = {true: '*true', false: '*false', null: '*null'} + exports.stringify = function(v, options) { var out = [], t, @@ -246,7 +248,10 @@ str += t sep = !(t === '!' || t === '(' || t === ')') } - if (!short) { + if (short) { + t = antiJSON[str] + if (t) str = t + } else { str += '~' } return str From bdd45b29bd3da9f86dd160511843df41ae374d1d Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Mon, 16 Jul 2018 21:53:06 +0200 Subject: [PATCH 28/37] allow JSON input --- README.md | 76 +++++++++++++++++++++++---------------------- __tests__/jsurl2.js | 9 ++++++ lib/jsurl2.js | 5 +-- 3 files changed, 51 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index fd9661a..33f41fb 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,17 @@ JSURL v2 aims to be a drop-in replacement for JSON encoding with better size and JSURL v2 has been designed to be -* Fast: our test case actually outperforms native JSON -* Compact: shorter output than JSON and v1 -* Readable: more readable than v1 in many cases as well as leaving accented characters unchanged (so they are readable in URLs and embeds) -* URI-ready: It does not encode everything that should be URI-encoded, but it does encode all delimiters of query strings. - * You can embed it as part of a query string, and normally you won't need to do any URI encoding yourself (browser/http client will take care of that). - * It will be correctly detected as part of the URI by most auto-URL-from-text implementations. -* embed-ready: - * embeddable in \