Skip to content

Commit 48db7f1

Browse files
committed
Initial commit
0 parents  commit 48db7f1

24 files changed

+976
-0
lines changed

.gitignore

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
lib-cov
2+
*.seed
3+
*.log
4+
*.csv
5+
*.dat
6+
*.out
7+
*.pid
8+
*.gz
9+
*.swp
10+
11+
pids
12+
logs
13+
results
14+
tmp
15+
16+
# Build
17+
public/css/main.css
18+
19+
# Coverage reports
20+
coverage
21+
22+
# API keys and secrets
23+
.env
24+
25+
# Dependency directory
26+
node_modules
27+
bower_components
28+
29+
# Editors
30+
.idea
31+
*.iml
32+
33+
# OS metadata
34+
.DS_Store
35+
Thumbs.db
36+
37+
# Ignore built ts files
38+
dist/**/*
39+
40+
# ignore yarn.lock
41+
yarn.lock
42+
.lh/

.vscode/launch.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Run Current File",
6+
"type": "node",
7+
"request": "launch",
8+
"runtimeExecutable": "node",
9+
"runtimeArgs": [
10+
"--nolazy",
11+
"-r",
12+
"ts-node/register/transpile-only",
13+
"-r",
14+
"tsconfig-paths/register"
15+
],
16+
"args": ["${workspaceFolder}/${relativeFile}"],
17+
"cwd": "${workspaceRoot}",
18+
"internalConsoleOptions": "openOnSessionStart",
19+
"skipFiles": ["<node_internals>/**", "node_modules/**"]
20+
},
21+
{
22+
"name": "Run Simple Test",
23+
"type": "node",
24+
"request": "launch",
25+
"runtimeExecutable": "node",
26+
"runtimeArgs": [
27+
"--nolazy",
28+
"-r",
29+
"ts-node/register/transpile-only",
30+
"-r",
31+
"tsconfig-paths/register"
32+
],
33+
"args": ["${workspaceFolder}/tests/simple.test.ts"],
34+
"cwd": "${workspaceRoot}",
35+
"internalConsoleOptions": "openOnSessionStart",
36+
"skipFiles": ["<node_internals>/**", "node_modules/**"]
37+
}
38+
]
39+
}

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"cSpell.words": [
3+
"RSON"
4+
]
5+
}

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "rsonjs",
3+
"version": "0.1.0",
4+
"main": "dist/index.js",
5+
"repository": "https://github.com/xpodev/rsonjs",
6+
"author": "Xpo Development",
7+
"license": "MIT",
8+
"devDependencies": {
9+
"@types/node": "^22.10.1",
10+
"ts-node": "^10.9.2",
11+
"tsconfig-paths": "^4.2.0",
12+
"typescript": "^5.7.2"
13+
}
14+
}

src/errors.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export class RSONError extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = "RSONError";
5+
}
6+
}
7+
8+
export class RSONParseError extends RSONError {
9+
constructor(message: string) {
10+
super(message);
11+
this.name = "RSONParseError";
12+
}
13+
}
14+

src/globals.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare type ReplacerFunction = ((key: string, value: any) => any) | null;

src/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Parser } from "./parser";
2+
import { StringStream } from "./stream/string";
3+
import { stringify as rsonStringify } from "./stringify";
4+
5+
namespace RSON {
6+
export function parse(input: string): any {
7+
if (input === "") {
8+
throw new SyntaxError("Unexpected end of RSON input");
9+
}
10+
return new Parser(new StringStream(input)).parse();
11+
}
12+
13+
export function stringify(
14+
input: any,
15+
replacer?: ReplacerFunction,
16+
space?: string | number
17+
): string {
18+
return rsonStringify(input, replacer, space);
19+
}
20+
}
21+
22+
export default RSON;

src/parser/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Parser } from "./parser";

src/parser/parser.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Token, Tokenizer, TokenType } from "src/tokenizer";
2+
import { IStream } from "src/stream/stream";
3+
import { TokenStream } from "./token-stream";
4+
import { RSONParseError } from "src/errors";
5+
6+
class LateReference {
7+
constructor(
8+
public readonly obj: Record<string, any> | Array<any>,
9+
public readonly key: number | string,
10+
public readonly name: string
11+
) {}
12+
}
13+
14+
export class Parser {
15+
private tokenizer: Tokenizer;
16+
private tokenStream!: TokenStream;
17+
private readonly objectReferences: Record<string, any> = {};
18+
private readonly lateReferences: Array<LateReference> = [];
19+
constructor(stream: IStream) {
20+
this.tokenizer = new Tokenizer(stream);
21+
}
22+
23+
public parse() {
24+
this.tokenStream = new TokenStream(this.tokenizer.tokenize());
25+
const obj = this.parseValue();
26+
this.eat(TokenType.EOF);
27+
this.lateReferences.forEach((ref) => {
28+
if (ref.name in this.objectReferences) {
29+
if (Array.isArray(ref.obj)) {
30+
ref.obj.splice(ref.key as number, 1, this.objectReferences[ref.name]);
31+
} else {
32+
ref.obj[ref.key as string] = this.objectReferences[ref.name];
33+
}
34+
} else {
35+
throw new RSONParseError(
36+
`Object reference '${ref.name}' not found in object references`
37+
);
38+
}
39+
});
40+
return obj;
41+
}
42+
43+
private parseValue() {
44+
const token = this.tokenStream.current();
45+
let value: any;
46+
switch (token.type) {
47+
case TokenType.LeftCurlyBracket:
48+
value = this.parseObject();
49+
break;
50+
case TokenType.LeftSquareBracket:
51+
value = this.parseArray();
52+
break;
53+
case TokenType.String:
54+
value = this.tokenStream.next().value;
55+
break;
56+
case TokenType.Number:
57+
value = Number(this.tokenStream.next().value);
58+
break;
59+
case TokenType.True:
60+
this.eat(TokenType.True);
61+
value = true;
62+
break;
63+
case TokenType.False:
64+
this.eat(TokenType.False);
65+
value = false;
66+
break;
67+
case TokenType.Null:
68+
this.eat(TokenType.Null);
69+
value = null;
70+
break;
71+
default:
72+
throw new RSONParseError(
73+
`Unexpected token ${token.value} at position ${token.startPosition}`
74+
);
75+
}
76+
77+
if (this.tokenStream.hasNext()) {
78+
if (
79+
this.tokenStream.current().type === TokenType.ObjectReferenceDefinition
80+
) {
81+
const objectReferenceName = this.tokenStream.next().value;
82+
if (objectReferenceName in this.objectReferences) {
83+
throw new RSONParseError(
84+
`Object reference '${objectReferenceName}' already exists`
85+
);
86+
}
87+
88+
this.objectReferences[objectReferenceName] = value;
89+
}
90+
}
91+
92+
return value;
93+
}
94+
95+
private parseObject() {
96+
this.eat(TokenType.LeftCurlyBracket);
97+
const obj: Record<string, any> = {};
98+
while (this.tokenStream.current().type !== TokenType.RightCurlyBracket) {
99+
const key = this.tokenStream.next().value;
100+
this.eat(TokenType.Colon);
101+
102+
if (this.tokenStream.current().type === TokenType.ObjectReference) {
103+
const ref = this.parseReference();
104+
if (ref instanceof Token) {
105+
this.lateReferences.push(
106+
new LateReference(obj, key, ref.value)
107+
);
108+
} else {
109+
obj[key] = ref;
110+
}
111+
} else {
112+
obj[key] = this.parseValue();
113+
}
114+
this.maybeEat(TokenType.Comma);
115+
}
116+
this.eat(TokenType.RightCurlyBracket);
117+
return obj;
118+
}
119+
120+
private parseArray() {
121+
this.eat(TokenType.LeftSquareBracket);
122+
const arr: any[] = [];
123+
while (this.tokenStream.current().type !== TokenType.RightSquareBracket) {
124+
if (this.tokenStream.current().type === TokenType.ObjectReference) {
125+
const ref = this.parseReference();
126+
if (ref instanceof Token) {
127+
this.lateReferences.push(
128+
new LateReference(arr, arr.length, ref.value)
129+
);
130+
} else {
131+
arr.push(ref);
132+
}
133+
} else {
134+
arr.push(this.parseValue());
135+
}
136+
this.maybeEat(TokenType.Comma);
137+
}
138+
this.eat(TokenType.RightSquareBracket);
139+
return arr;
140+
}
141+
142+
private parseReference() {
143+
const referenceToken = this.tokenStream.next();
144+
if (referenceToken.value in this.objectReferences) {
145+
return this.objectReferences[referenceToken.value];
146+
} else {
147+
return referenceToken;
148+
}
149+
}
150+
151+
private maybeEat(tokenType: TokenType) {
152+
if (this.tokenStream.current().type === tokenType) {
153+
this.eat(tokenType);
154+
}
155+
}
156+
157+
private eat(tokenType: TokenType) {
158+
const token = this.tokenStream.current();
159+
if (token.type !== tokenType) {
160+
throw new RSONParseError(
161+
`Unexpected token ${token.value} at position ${token.startPosition}`
162+
);
163+
}
164+
return this.tokenStream.next();
165+
}
166+
}

src/parser/token-stream.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Token } from "src/tokenizer";
2+
3+
export class TokenStream {
4+
private tokens: Token[];
5+
private position = 0;
6+
7+
constructor(tokens: Token[]) {
8+
this.tokens = tokens;
9+
}
10+
11+
current() {
12+
return this.tokens[this.position];
13+
}
14+
15+
next() {
16+
return this.tokens[this.position++];
17+
}
18+
19+
hasNext() {
20+
return this.position < this.tokens.length;
21+
}
22+
}

0 commit comments

Comments
 (0)