Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jsurl v2 #17

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1c8edbc
Switch to AVA for tests
wmertens Apr 1, 2017
4ffcdea
JSURL2
wmertens Apr 1, 2017
15cdfea
v2 rules update
wmertens Apr 2, 2017
fd512d6
Make sure individual imports work in ES6
wmertens Apr 2, 2017
b6ac1e2
Poor man's performance "test"
wmertens Apr 2, 2017
1b6af29
v2: make ~ on last element of object optional
wmertens Apr 2, 2017
bea30da
Add performance.html to use chrome profiling tools
wmertens Apr 2, 2017
ed7f6b6
Add `short` option and fix `true` in objects
wmertens Apr 2, 2017
5986de0
Performance improvements
wmertens Apr 2, 2017
745f054
:tada: now outperforms JSON on size *and* speed!
wmertens Apr 2, 2017
52220c4
lint
wmertens Apr 2, 2017
47c1b8f
v2: Support .toJSON() as per JSON spec
wmertens Apr 12, 2017
e220bd2
v2: Only special-case Date if `rich: true`
wmertens Apr 12, 2017
091299e
travis: Only check on v4 and latest NodeJS
wmertens Apr 12, 2017
137555f
v2: Escape chars for URI and <script> encoding
wmertens Apr 12, 2017
2f18dbd
Add some escaping test cases
wmertens Apr 12, 2017
b809244
v2: pass options via `tryParse`
wmertens Apr 12, 2017
b84a829
Make v2 default, and v1 optional as `jsurl/v1`
wmertens Apr 12, 2017
abaf43c
Document v2
wmertens Apr 12, 2017
f599ebd
Some changes for bower and presumed needed for npm
wmertens Apr 12, 2017
42756e3
attempt to fix npm install of branch
wmertens Jun 11, 2017
88030a2
fix NaN parse on bad number
wmertens Feb 2, 2018
a124052
prettier
wmertens Feb 2, 2018
7e66fe0
Protect against Objects with null prototype
wmertens Jun 2, 2018
b4c63ea
use Jest for tests
wmertens Jul 12, 2018
8cc227c
fix performance page
wmertens Jul 12, 2018
b9900c5
Make sure JSON can parse output only if same value
wmertens Jul 16, 2018
bdd45b2
allow JSON input
wmertens Jul 16, 2018
c6db093
deURI: also strip whitespace
wmertens Aug 19, 2018
b6174ca
Readme: only discuss v2
wmertens Aug 19, 2018
7c741b0
`rich:false` generates same objects as JSON
wmertens Aug 19, 2018
f42c750
Merge json into jsurl2
wmertens Aug 19, 2018
c3418dc
test: oops
wmertens Aug 19, 2018
b35881f
rich: NaN, +-Infinity
wmertens Aug 19, 2018
083ff89
clean: concat is slightly faster than join
wmertens Aug 19, 2018
edce7d8
Make Travis work
wmertens Oct 31, 2018
22e6524
Escape Unicode Character 'LINE SEPARATOR' (U+2028)
wmertens Nov 19, 2018
6d5714b
tests: add Big List of Naughty Strings
wmertens Nov 19, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
language: node_js
node_js:
- "4.2"
- "0.12"
- "0.10"
- "lts/*"
sudo: false
156 changes: 109 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,138 @@
## JSURL
# JSURL

JSURL aims to be a drop-in replacement for JSON encoding with better size and time characteristics.

JSURL has been designed to be

- Fast: our test case actually outperforms native JSON
- Compact: shorter output than JSON
- Readable: it leaves accented characters unchanged and doesn't add much characters for encoding
- 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.
- Embeddable:
- You can safely put it in `<script>` tags inside single-quoted Javascript strings (unlike JSON)
- Added whitespace can simply be removed, it is not used
- Extensible:
- `rich` mode encodes/decodes `Date` objects, `undefined`, `NaN` and `Infinity`
- Future: The encoding dictionary can be extended, providing more space optimization
- Future: You can provide your own stringify/parse for custom objects
- Easy upgrade:
- Simply replace `JSON.parse` with `JSURL.parse` and `JSON.stringify` with `JSURL.stringify`
- Parses JSON as well
- A stringify/parse cycle generates the same output as with JSON
- (one exception: it always returns a string, even when JSON would return `undefined`)
- It will never generate valid JSON except when it's the correct representation

Given its speed and size, it is well-suited to pass JS values to scripts in HTML, like initial data after Server-Side-Rendering. To do so, embed the result inside a single-quoted string (not double-quoted) and parse that in your script.

Some room has been left in the encoding space for special values. If you enable `rich` on the stringifier, it will encode JS Date objects so that they decode as JS Date objects, and later it might support custom encode/decode of your own object types.

JSURL is an alternative to JSON + URL encoding (or JSON + base64 encoding).
It makes it handy to pass complex values via URL query parameters.

JSURL has been designed to be:

* Compact: its output is much more compact than JSON + URL encoding (except in pathological cases).
It is even often slightly more compact than regular JSON!
* Readable: its output is much more readable than JSON + URL encoding.
* Foolproof: its output only contains characters that are unaffected by URL encoding/decoding.
There is no risk of missing a URL encoding/decoding pass, or of messing up a JSURL string by applying
an extra URL encoding or decoding pass.
* Easy to generate and parse

## Syntax

Think of it as JSON with the following changes:
## Examples

* Curly braces (`{` and `}`) replaced by parentheses (`(` and `)`)
* Square brackets (`[` and `]`) replaced by `(~` and `)`
* Property names unquoted (but escaped -- see below).
* String values prefixed by a single quote (`'`) and escaped
* All other JSON punctuation (colon `:` and comma `,`) replaced by tildes (`~`)
* An extra tilde (`~`) at the very beginning.
JSON:

Property names and string values are escaped as follows:
```json
{"name": "John Doé", "age": 42, "user": true, "children": ["Mary", "Bill"]}
```

* Letters, digits, underscore (`_`), hyphen (`-`) and dot (`.`) are preserved.
* Dollar sign (`$`) is replaced by exclamation mark (`!`)
* Other characters with UNICODE value <= `0xff` are encoded as `*XX`
* Characters with UNICODE value > `0xff` are encoded as `**XXXX`
JSON + URI encoding:

## Examples
```text
%7B%22name%22%3A%22John%20Do%C3%22%2C%22age%22%3A42%2C%22user%22%3Atrue%2C%22children%22%3A%5B%22Mary%22%2C%22Bill%22%5D%7D
```

JSON:
JSURL:

``` json
{"name":"John Doe","age":42,"children":["Mary","Bill"]}
```jsurl
(name~John_Doé~age~42~user~~children~!Mary~Bill)~
```

JSON + URL encoding:
JSURL + URI encoding:

```
%7B%22name%22%3A%22John%20Doe%22%2C%22age%22%3A42%2C%22children%22%3A%5B%22Mary%22%2C%22Bill%22%5D%7D
```text
(name~John_Do%C3%A9~age~42~user~~children~!Mary~Bill)~
```

JSURL:
## Installing

The easiest way to install `jsurl` is with NPM:

``` jsurl
~(name~'John*20Doe~age~42~children~(~'Mary~'Bill))
```sh
npm install jsurl
```

## API

``` javascript
```javascript
var JSURL = require("jsurl");

str = JSURL.stringify(obj);
obj = JSURL.parse(str);
// Options:
// * `rich`: encode Date, `undefined`, `Infinity`
// * `short`: remove optional trailing delimiters
str = JSURL.stringify(obj[, options]);

// return def instead of throwing on error
obj = JSURL.tryParse(str[, def]);
// Options:
// * `deURI`: remove URI encoding and whitespace
obj = JSURL.parse(str[, options]);

// return `default` instead of throwing on error; options are passed to `parse()`
obj = JSURL.tryParse(str[, default][, options]);
```

# Installation
### Upgrading from version 1

The easiest way to install `jsurl` is with NPM:
- Version 2 might need extra URI-encoding depending on your needs
- You can call `parse(text, {deURI: true})` to handle any URI decoding automatically. This is slower, so only do this if needed.
- Version 1 encoded text is parsed by v2
- Version 1 clients will _not_ be able to parse version 2 encoded text, be careful when deploying new encoded text

```sh
npm install jsurl
### Version 1

The old v1 API is still available if you need to decode v1 stringifieds.

```javascript
var JSURL1 = require("jsurl/v1");

str = JSURL1.stringify(obj);
obj = JSURL1.parse(str);

// return def instead of throwing on error
obj = JSURL1.tryParse(str[, def]);
```

## Syntax

JSURL uses the allowable characters in URI schemes for multiple purposes depending on the location in the result. Some examples:

- `!` starts an array if it is the first character in a value, but inside a string it is unchanged.
- `~` and `)` are used as end-of-value and end-of-object delimiters, and are illegal inside encoded values.
- `*` starts a string, but can be left out if the first string character is a-z. Inside a string, it escapes special characters.
- whitespace is never legal, so you can strip it before parsing if it can be introduced accidentally

JSURL has a `short` mode, which omits the unnecessary ending delimiters. You can use this to save a few more bytes, but you won't be able to spot an encoded value on sight by the ending `~`.

Since browsers may choose to encode any character with URI escaping, and special characters are shown in URLs, no attempt is made to make v2 URI-neutral. Decoding will work no matter how many encodings happened, if you pass the `deURI: true` option to the parser.

## Roadmap

- Indenting
- `stringify` adds whitespace (post-process, too slow in parser code)
- Encoding dictionary
- Pass to `parse` and `stringify`
- Allow embedding dictionary in encoded output
- Only embed used keys
- Auto mode that encodes object keys with length > 4
- Custom encoding
- Dictionary of encoders that get called on Objects
- First one to return a value other than `undefined` wins
- Decoders get called on their encoded values
- `new JSURL`-based API to prepare custom encoding and dictionary
- Interactive demo webpage
- Examples that you can edit with JSON/JSURL side-by-side
- For each example, compare with JSON: length, length when URI encoded, stringify speed, parse speed
- Benchmark: generates random objects to encode, average space and time savings

## License

This work is licensed under the [MIT license](http://en.wikipedia.org/wiki/MIT_License).
132 changes: 132 additions & 0 deletions __tests__/jsurl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const {stringify, parse, tryParse} = require('../v1')

// test macro, both directions
const cmp = (v, s) => {
// regular
expect(stringify(v)).toBe(s)
// roundtrip
expect(stringify(parse(s))).toBe(s)
}

// basic values
test('basics', () => {
cmp(undefined, undefined)
cmp(function() {
foo()
}, undefined)
cmp(null, '~null')
cmp(false, '~false')
cmp(true, '~true')
cmp(0, '~0')
cmp(1, '~1')
cmp(-1.5, '~-1.5')
cmp('hello world\u203c', "~'hello*20world**203c")
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
cmp(NaN, '~null')
cmp(Infinity, '~null')
cmp(-Infinity, '~null')

// arrays
cmp([], '~(~)')
cmp(
[
undefined,
function() {
foo()
},
null,
false,
0,
'hello world\u203c',
],
"~(~null~null~null~false~0~'hello*20world**203c)"
)

// objects
cmp({}, '~()')
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
cmp(
{
a: [[1, 2], [], {}],
b: [],
c: {
d: 'hello',
e: {},
f: [],
},
},
"~(a~(~(~1~2)~(~)~())~b~(~)~c~(d~'hello~e~()~f~(~)))"
)
})

test('percent-escaped single quotes', () => {
expect(parse('~(a~%27hello~b~%27world)')).toEqual({
a: 'hello',
b: 'world',
})
})

test('percent-escaped percent-escaped single quotes', () => {
expect(parse('~(a~%2527hello~b~%2525252527world)')).toEqual({
a: 'hello',
b: 'world',
})
})

test('tryParse', () => {
expect(tryParse('~null')).toBe(null)
expect(tryParse('~1', 2)).toBe(1)
expect(tryParse('1')).toBe(undefined)
expect(tryParse('1', 0)).toBe(0)
})

test('parse performance', () => {
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`)
expect(ms < 300).toBe(true)
})

test('stringify performance', () => {
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`)
expect(ms < 300).toBe(true)
})
Loading