Skip to content

Commit 8d60f69

Browse files
feat: better directive parsing
1 parent 0153317 commit 8d60f69

File tree

4 files changed

+87
-30
lines changed

4 files changed

+87
-30
lines changed

src/framework/typescript/directives/Directive.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ export type ParserState = {
55
output: string;
66
input: string;
77
data: Record<string, unknown>;
8+
currentArgument: string | undefined;
89
};
910

10-
abstract class Directive {
11+
abstract class Directive<T = unknown> {
1112
public abstract readonly name: string;
1213

1314
public abstract apply(
1415
parser: DirectiveParser,
1516
parserState: ParserState,
16-
arg: string
17+
arg: T
1718
): Awaitable<void>;
1819
}
1920

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type Directive from "@framework/directives/Directive";
22
import type { Class } from "@framework/types/Utils";
33
import { isAlpha } from "@framework/utils/string";
4+
import DirectiveParseError from "@framework/directives/DirectiveParseError";
5+
import JSON5 from "json5";
46

57
class DirectiveParser {
68
private readonly availableDirectives: Array<Directive> = [];
@@ -13,47 +15,104 @@ class DirectiveParser {
1315
this.availableDirectives.push(new directive());
1416
}
1517

16-
public async parse(input: string) {
17-
const lines = input.trim().split(/\s*\n\s*/);
18+
public async parse(input: string, silent = true) {
1819
const state = {
19-
lines,
2020
input,
2121
output: input,
22-
data: {} as Record<string, unknown>
22+
data: {} as Record<string, unknown>,
23+
currentArgument: undefined as string | undefined
2324
};
2425

25-
for (const line of lines) {
26-
if (!line.startsWith("@")) {
26+
for (let i = 0; i < input.length; i++) {
27+
if (input[i] !== "@") {
2728
continue;
2829
}
2930

3031
let name = "";
3132

32-
while (isAlpha(line[name.length + 1])) {
33-
name += line[name.length + 1];
33+
while (isAlpha(input[i + name.length + 1])) {
34+
name += input[i + name.length + 1];
3435
}
3536

3637
if (!name) {
3738
continue;
3839
}
3940

40-
if (!line.endsWith(")")) {
41+
const sliced = input.slice(i + name.length + 1).trim();
42+
const directive = this.availableDirectives.find(d => d.name === name);
43+
44+
if (!directive) {
4145
continue;
4246
}
4347

44-
const sliced = line.slice(name.length + 1).trim();
45-
const arg = sliced.slice(1, sliced.length - 1);
46-
const directive = this.availableDirectives.find(d => d.name === name);
48+
let arg;
49+
let length = 0;
50+
51+
try {
52+
const { json, length: computedLength, str } = this.getNextJSON5Literal(sliced, name);
53+
arg = json;
54+
length = computedLength;
55+
state.currentArgument = str;
56+
}
57+
catch (error) {
58+
if (!silent) {
59+
throw error;
60+
}
4761

48-
if (!directive) {
4962
continue;
5063
}
5164

5265
await directive.apply(this, state, arg);
66+
i += name.length + 1 + length - 1;
67+
state.currentArgument = undefined;
5368
}
5469

5570
return state;
5671
}
72+
73+
public getNextJSON5Literal(input: string, directiveName: string) {
74+
const start = input.indexOf("{");
75+
let end = start + 1;
76+
let depth = 1;
77+
78+
while (depth > 0) {
79+
if (input[end] === "{") {
80+
depth++;
81+
} else if (input[end] === "}") {
82+
depth--;
83+
}
84+
else if (input[end] === undefined) {
85+
throw new DirectiveParseError("Unexpected end of input");
86+
}
87+
else if (input[end] === "\"") {
88+
end = input.indexOf("\"", end + 1);
89+
}
90+
else if (input[end] === "'") {
91+
end = input.indexOf("'", end + 1);
92+
}
93+
94+
if (end === -1) {
95+
throw new DirectiveParseError("Unexpected end of input");
96+
}
97+
98+
end++;
99+
}
100+
101+
const str = input.slice(start, end);
102+
103+
try {
104+
return {
105+
json: JSON5.parse(str),
106+
length: str.length,
107+
str
108+
};
109+
}
110+
catch (error) {
111+
throw new DirectiveParseError("Failed to parse JSON5 literal in directive: " + directiveName, {
112+
cause: error
113+
});
114+
}
115+
}
57116
}
58117

59118
export default DirectiveParser;

src/framework/typescript/utils/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20+
export function escapeRegex(string: string) {
21+
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
22+
}
23+
2024
export function requireNonNull<T>(value: T | null | undefined, message?: string): T {
2125
if (value === null || value === undefined) {
2226
throw new Error(message ?? "Value cannot be null or undefined");

src/main/typescript/directives/EmbedDirective.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import Directive from "@framework/directives/Directive";
33
import DirectiveParseError from "@framework/directives/DirectiveParseError";
44
import type DirectiveParser from "@framework/directives/DirectiveParser";
55
import type { APIEmbed } from "discord.js";
6-
import JSON5 from "json5";
76
import { z } from "zod";
7+
import { escapeRegex } from "@framework/utils/utils";
88

9-
class EmbedDirective extends Directive {
9+
class EmbedDirective extends Directive<APIEmbed> {
1010
public override readonly name = "embed";
1111
public static readonly discordApiEmbedSchema = z.object({
1212
title: z.string().optional(),
@@ -56,18 +56,8 @@ class EmbedDirective extends Directive {
5656
.optional()
5757
});
5858

59-
public override async apply(parser: DirectiveParser, state: ParserState, arg: string) {
60-
let parsed;
61-
62-
try {
63-
parsed = JSON5.parse(arg);
64-
} catch (error) {
65-
throw new DirectiveParseError("Invalid argument: must be valid JSON5", {
66-
cause: error
67-
});
68-
}
69-
70-
const embed = EmbedDirective.discordApiEmbedSchema.safeParse(parsed);
59+
public override async apply(parser: DirectiveParser, state: ParserState, arg: APIEmbed) {
60+
const embed = EmbedDirective.discordApiEmbedSchema.safeParse(arg);
7161

7262
if (!embed.success) {
7363
throw new DirectiveParseError(
@@ -82,7 +72,10 @@ class EmbedDirective extends Directive {
8272
state.output += "\n";
8373
}
8474

85-
state.output = state.output.replace(/@embed\s*\([^\n]+\)\n/gi, "");
75+
if (state.currentArgument) {
76+
state.output = state.output.replace(new RegExp(`@embed(\\s*)\\((\\s*)${escapeRegex(state.currentArgument)}(\\s*)\\)`), "");
77+
}
78+
8679
state.data.embeds ??= [];
8780
(state.data.embeds as Array<APIEmbed>)!.push(embed.data);
8881
}

0 commit comments

Comments
 (0)