Skip to content

Instantly share code, notes, and snippets.

@hi-ogawa
Created January 23, 2022 01:43
Show Gist options
  • Save hi-ogawa/af3141c6bb497a1c8451eb2f265b1c38 to your computer and use it in GitHub Desktop.
Save hi-ogawa/af3141c6bb497a1c8451eb2f265b1c38 to your computer and use it in GitHub Desktop.
url-json.ts
/*
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