Created
January 23, 2022 01:43
-
-
Save hi-ogawa/af3141c6bb497a1c8451eb2f265b1c38 to your computer and use it in GitHub Desktop.
url-json.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
URL safe json | |
cf. | |
- https://www.json.org/json-en.html | |
- https://github.com/Sage/jsurl | |
- https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set | |
- ASCII alphanumeric, U+002A (*), U+002D (-), U+002E (.), and U+005F (_) | |
AST | |
<value> | |
<object> | |
<array> | |
<tagged-primitive> | |
<object> | |
_~ <members> ~_ | |
<members> | |
<member> | |
<member> . <members> | |
<member> | |
* <string> . * - <value> | |
<array> | |
.~ <values> ~. | |
<values> | |
<value> | |
<value> . <values> | |
<tagged-primitive> | |
* <primitive> * | |
<primitive> | |
<string> . | |
<number> _ | |
true | |
false | |
null | |
Token | |
_~ | |
~_ | |
.~ | |
~. | |
- | |
. | |
<tagged-primitive> | |
*/ | |
const OBJECT_L = "_~"; | |
const OBJECT_R = "~_"; | |
const OBJECT_REL = "-"; | |
const OBJECT_SEP = "."; | |
const ARRAY_L = ".~"; | |
const ARRAY_R = "~."; | |
const ARRAY_SEP = "."; | |
const PRIMITIVE_L = "*"; | |
const PRIMITIVE_R = "*"; | |
const STRING_TAG = "."; | |
const NUMBER_TAG = "_"; | |
export function stringify(data: any): string { | |
// | |
// Primitives | |
// | |
if (data === true) { | |
return PRIMITIVE_L + "true" + PRIMITIVE_R; | |
} | |
if (data === false) { | |
return PRIMITIVE_L + "false" + PRIMITIVE_R; | |
} | |
if (data === null) { | |
return PRIMITIVE_L + "null" + PRIMITIVE_R; | |
} | |
if (typeof data === "string") { | |
return ( | |
PRIMITIVE_L + | |
JSON.stringify(data) | |
.slice(1, -1) | |
.replace(PRIMITIVE_R, "\\" + PRIMITIVE_R) + | |
STRING_TAG + | |
PRIMITIVE_R | |
); | |
} | |
if (typeof data === "number") { | |
return PRIMITIVE_L + JSON.stringify(data) + NUMBER_TAG + PRIMITIVE_R; | |
} | |
// | |
// Array | |
// | |
if (Array.isArray(data)) { | |
return ARRAY_L + data.map(stringify).join(ARRAY_SEP) + ARRAY_R; | |
} | |
// | |
// Object | |
// | |
const members: string[] = []; | |
for (const [key, value] of Object.entries(data)) { | |
if (typeof value === "undefined") { | |
continue; | |
} | |
members.push(stringify(key) + OBJECT_REL + stringify(value)); | |
} | |
return OBJECT_L + members.join(OBJECT_SEP) + OBJECT_R; | |
} | |
class Parser { | |
private cursor: number = 0; | |
constructor(private text: string) {} | |
match(prefix: string): boolean { | |
return this.text.slice(this.cursor).startsWith(prefix); | |
} | |
consume(expected: string) { | |
assert.ok(this.match(expected)); | |
this.cursor += expected.length; | |
} | |
parse(): any { | |
if (this.match(PRIMITIVE_L)) { | |
return this.parsePrimitive(); | |
} | |
if (this.match(ARRAY_L)) { | |
return this.parseArray(); | |
} | |
if (this.match(OBJECT_L)) { | |
return this.parseObject(); | |
} | |
assert.ok(false); | |
} | |
parsePrimitive(): any { | |
const start = this.cursor; | |
let tagged: string | undefined = undefined; | |
for (let i = start + 1; i < this.text.length; i++) { | |
if (this.text[i] === "\\") { | |
i++; | |
continue; | |
} | |
if (this.text[i] === PRIMITIVE_R) { | |
tagged = this.text.substring(start, i + 1); | |
break; | |
} | |
} | |
assert.ok(typeof tagged === "string"); | |
this.consume(tagged); | |
if (tagged === PRIMITIVE_L + "true" + PRIMITIVE_R) { | |
return true; | |
} | |
if (tagged === PRIMITIVE_L + "false" + PRIMITIVE_R) { | |
return false; | |
} | |
if (tagged === PRIMITIVE_L + "null" + PRIMITIVE_R) { | |
return null; | |
} | |
if (tagged.endsWith(STRING_TAG + PRIMITIVE_R)) { | |
const slice_l = PRIMITIVE_L.length; | |
const slice_r = STRING_TAG.length + PRIMITIVE_L.length; | |
const data = JSON.parse( | |
'"' + | |
tagged | |
.slice(slice_l, -slice_r) | |
.replace("\\" + PRIMITIVE_R, PRIMITIVE_R) + | |
'"' | |
); | |
assert.ok(typeof data === "string"); | |
return data; | |
} | |
if (tagged.endsWith(NUMBER_TAG + PRIMITIVE_R)) { | |
const slice_l = PRIMITIVE_L.length; | |
const slice_r = NUMBER_TAG.length + PRIMITIVE_L.length; | |
const data = JSON.parse(tagged.slice(slice_l, -slice_r)); | |
assert.ok(typeof data === "number"); | |
return data; | |
} | |
assert.ok(false, ""); | |
} | |
parseArray(): any { | |
let first = true; | |
let result = []; | |
this.consume(ARRAY_L); | |
while (!this.match(ARRAY_R)) { | |
if (first) { | |
first = false; | |
} else { | |
this.consume(ARRAY_SEP); | |
} | |
const element = this.parse(); | |
result.push(element); | |
} | |
this.consume(ARRAY_R); | |
return result; | |
} | |
parseObject(): any { | |
let first = true; | |
let result: Record<string, any> = {}; | |
this.consume(OBJECT_L); | |
while (!this.match(OBJECT_R)) { | |
if (first) { | |
first = false; | |
} else { | |
this.consume(OBJECT_SEP); | |
} | |
const key = this.parse(); | |
assert.ok(typeof key === "string"); | |
this.consume(OBJECT_REL); | |
const value = this.parse(); | |
result[key] = value; | |
} | |
this.consume(OBJECT_R); | |
return result; | |
} | |
} | |
export function parse(stringified: string): any { | |
return new Parser(stringified).parse(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment