Skip to content

Commit 679e5b0

Browse files
committed
feat: Add Parser, Cursor and Tokens.
1 parent 23cfbaf commit 679e5b0

File tree

4 files changed

+261
-0
lines changed

4 files changed

+261
-0
lines changed

src/Cursor.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
export class Cursor {
2+
public index = -1;
3+
4+
public constructor(public input: string) {}
5+
6+
public get hasNext() {
7+
return this.index < this.input.length - 1;
8+
}
9+
10+
public get hasPrevious() {
11+
return this.index > 0;
12+
}
13+
14+
public consumeRemaining() {
15+
let result = '';
16+
while (this.hasNext) result += this.next();
17+
18+
return result;
19+
}
20+
21+
public consumeWhile(predicate: (char: string) => boolean) {
22+
let result: string | undefined = undefined;
23+
24+
while (this.hasNext && predicate(this.peekNext()!)) result = (result ?? '') + this.next();
25+
26+
return result;
27+
}
28+
29+
public skipWhitespace() {
30+
if (this.peekNext() !== ' ') return false;
31+
32+
while (this.peekNext() === ' ') this.next();
33+
34+
return true;
35+
}
36+
37+
public next() {
38+
if (this.hasNext) {
39+
this.index++;
40+
return this.input[this.index];
41+
}
42+
}
43+
44+
public previous() {
45+
if (this.hasPrevious) {
46+
this.index--;
47+
return this.input[this.index];
48+
}
49+
}
50+
51+
public peek() {
52+
return this.input[this.index];
53+
}
54+
55+
public peekNext() {
56+
return this.hasNext ? this.input[this.index + 1] : undefined;
57+
}
58+
59+
public peekPrevious() {
60+
return this.hasPrevious ? this.input[this.index - 1] : undefined;
61+
}
62+
}

src/Parser.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import {Cursor} from './Cursor';
2+
import {NamedArgumentToken, PositionalArgumentToken} from './Tokens';
3+
4+
export class Parser {
5+
public cursor = new Cursor(this.input);
6+
7+
public constructor(public input: string) {}
8+
9+
public get hasNext() {
10+
return this.cursor.hasNext;
11+
}
12+
13+
public parseNamed() {
14+
const tokens: NamedArgumentToken[] = [];
15+
let buffer = '';
16+
let outputBuffer = '';
17+
let isQuoted = false;
18+
let isFlag = false;
19+
let isFlagValue = false;
20+
let flagName = '';
21+
let isKeyword = false;
22+
let keywordName = '';
23+
24+
while (this.cursor.hasNext) {
25+
const char = this.cursor.next();
26+
27+
const canBeQuoted = !isQuoted && (!(isFlag && !isFlag) || isKeyword);
28+
29+
if (char === '"' && buffer.length === 0 && canBeQuoted) {
30+
isQuoted = true;
31+
continue;
32+
}
33+
34+
if (char === '-' && this.cursor.peekNext() === '-' && buffer.length === 0 && !isFlag && !isKeyword) {
35+
this.cursor.next();
36+
isFlag = true;
37+
continue;
38+
}
39+
40+
if (char === '=' && buffer.length > 0 && !isKeyword && !isFlag) {
41+
keywordName = buffer;
42+
buffer = '';
43+
isKeyword = true;
44+
continue;
45+
}
46+
47+
if (char === '\\' && this.cursor.peekNext() === '"' && isQuoted) {
48+
buffer += '"';
49+
this.cursor.next();
50+
continue;
51+
}
52+
53+
if (char === '"' && isQuoted) {
54+
if (isFlagValue) {
55+
tokens.push(new NamedArgumentToken(flagName, buffer));
56+
flagName = '';
57+
isFlag = false;
58+
isFlagValue = false;
59+
} else if (isKeyword) {
60+
tokens.push(new NamedArgumentToken(keywordName, buffer));
61+
keywordName = '';
62+
isKeyword = false;
63+
} else {
64+
outputBuffer += `\"${buffer}\"`;
65+
this.cursor.skipWhitespace();
66+
}
67+
68+
buffer = '';
69+
isQuoted = false;
70+
continue;
71+
}
72+
73+
if (char === ' ' && !isQuoted) {
74+
if (isFlag) {
75+
if (!isFlagValue) {
76+
flagName = buffer;
77+
isFlagValue = true;
78+
} else {
79+
tokens.push(new NamedArgumentToken(flagName, buffer));
80+
this.cursor.skipWhitespace();
81+
flagName = '';
82+
isFlag = false;
83+
isFlagValue = false;
84+
}
85+
} else if (isKeyword) {
86+
tokens.push(new NamedArgumentToken(keywordName, buffer));
87+
this.cursor.skipWhitespace();
88+
keywordName = '';
89+
isKeyword = false;
90+
} else {
91+
outputBuffer += `${buffer} `;
92+
}
93+
94+
buffer = '';
95+
continue;
96+
}
97+
98+
buffer += char;
99+
}
100+
101+
if (buffer.length > 0 && isFlag)
102+
if (isFlagValue) tokens.push(new NamedArgumentToken(flagName, buffer));
103+
else if (isKeyword) tokens.push(new NamedArgumentToken(keywordName, buffer));
104+
else outputBuffer += buffer;
105+
106+
this.cursor = new Cursor(outputBuffer.trim());
107+
108+
return tokens;
109+
}
110+
111+
public peekNext() {
112+
const currentIndex = this.cursor.index;
113+
const token = this.parseNext();
114+
this.cursor.index = currentIndex;
115+
116+
return token;
117+
}
118+
119+
public parseNext() {
120+
let token: PositionalArgumentToken | undefined;
121+
let buffer = '';
122+
let isQuoted = false;
123+
124+
while (this.cursor.hasNext) {
125+
const char = this.cursor.next();
126+
127+
if (char === '"' && buffer.length === 0 && !isQuoted) {
128+
isQuoted = true;
129+
continue;
130+
}
131+
132+
if (char === '\\' && this.cursor.peekNext() === '"' && isQuoted) {
133+
buffer += '"';
134+
this.cursor.next();
135+
continue;
136+
}
137+
138+
if (char === '"' && isQuoted) {
139+
token = new PositionalArgumentToken(buffer);
140+
this.cursor.skipWhitespace();
141+
buffer = '';
142+
break;
143+
}
144+
145+
if (char === ' ' && !isQuoted) {
146+
token = new PositionalArgumentToken(buffer);
147+
this.cursor.skipWhitespace();
148+
buffer = '';
149+
break;
150+
}
151+
152+
buffer += char;
153+
}
154+
155+
if (buffer.length > 0) token = new PositionalArgumentToken(buffer);
156+
157+
return token;
158+
}
159+
160+
public consumeRemaining() {
161+
return this.cursor.consumeRemaining();
162+
}
163+
164+
public consumeWhile(predicate: (char: string) => boolean) {
165+
const result = this.cursor.consumeWhile(predicate);
166+
if (result) this.cursor.skipWhitespace();
167+
return result;
168+
}
169+
170+
public peekRemaining() {
171+
const currentIndex = this.cursor.index;
172+
const result = this.cursor.consumeRemaining();
173+
this.cursor.index = currentIndex;
174+
175+
return result;
176+
}
177+
178+
public peekWhile(predicate: (char: string) => boolean) {
179+
const currentIndex = this.cursor.index;
180+
const result = this.cursor.consumeWhile(predicate);
181+
this.cursor.index = currentIndex;
182+
183+
return result;
184+
}
185+
}

src/Tokens.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface Token<T> {
2+
readonly data: T;
3+
}
4+
5+
export class PositionalArgumentToken implements Token<string> {
6+
public constructor(public data: string) {}
7+
}
8+
9+
export class NamedArgumentToken implements Token<string> {
10+
public constructor(public name: string, public data: string) {}
11+
}

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './Cursor';
2+
export * from './Parser';
3+
export * from './Tokens'

0 commit comments

Comments
 (0)