1import * as Schema from "effect/Schema";
2
3const decodeString = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.String));
4
5type Token = { value: string; start: number; end: number; quoted: boolean };
6export type KdlNode = {
7 name: string;
8 header: Token[];
9 children: KdlNode[];
10 start: number;
11 end: number;
12 close: number | undefined;
13};
14
15export function readKdlNodes(source: string): KdlNode[] {
16 const tokens: Token[] = [];
17 let offset = 0;
18 const invalid = () =>
19 new Error("Couldn't read this Niri config. Use Advanced to configure it manually.");
20 while (offset < source.length) {
21 const start = offset;
22 const rest = source.slice(offset);
23 if (/^[\t \r\uFEFF\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/.test(rest)) {
24 offset++;
25 continue;
26 }
27 if (rest.startsWith("//")) {
28 const end = source.indexOf("\n", offset);
29 offset = end < 0 ? source.length : end;
30 continue;
31 }
32 if (rest.startsWith("/*")) {
33 offset += 2;
34 let depth = 1;
35 while (depth && offset < source.length) {
36 if (source.startsWith("/*", offset)) {
37 depth++;
38 offset += 2;
39 } else if (source.startsWith("*/", offset)) {
40 depth--;
41 offset += 2;
42 } else offset++;
43 }
44 if (depth) throw invalid();
45 continue;
46 }
47 const continuation = /^\\[\t \r\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]*\n/.exec(rest);
48 if (continuation) {
49 offset += continuation[0].length;
50 continue;
51 }
52 const raw = /^r(#{0,32})"/.exec(rest);
53 if (raw) {
54 const terminator = `"${raw[1]}`;
55 const end = source.indexOf(terminator, offset + raw[0].length);
56 if (end < 0) throw invalid();
57 offset = end + terminator.length;
58 tokens.push({
59 value: source.slice(start + raw[0].length, end),
60 start,
61 end: offset,
62 quoted: true,
63 });
64 continue;
65 }
66 if (rest[0] === '"') {
67 offset++;
68 while (offset < source.length && source[offset] !== '"') {
69 if (source[offset] === "\\") offset++;
70 offset++;
71 }
72 if (offset === source.length) throw invalid();
73 offset++;
74 // KDL's unicode escapes differ from JSON; names needing those stay on the manual path.
75 let value: string;
76 try {
77 value = decodeString(source.slice(start, offset));
78 } catch {
79 throw invalid();
80 }
81 tokens.push({ value, start, end: offset, quoted: true });
82 continue;
83 }
84 if (rest.startsWith("/-")) offset += 2;
85 else if ("{};\n=()".includes(rest[0]!)) offset++;
86 else {
87 while (offset < source.length && !/[\s{};="()]/.test(source[offset]!)) {
88 if (["//", "/*", "/-"].some((prefix) => source.startsWith(prefix, offset))) break;
89 offset++;
90 }
91 }
92 if (offset === start) throw invalid();
93 tokens.push({ value: source.slice(start, offset), start, end: offset, quoted: false });
94 }
95 let cursor = 0;
96 const is = (token: Token | undefined, value: string) =>
97 token && !token.quoted && token.value === value;
98 const nodes = (nested: boolean): KdlNode[] => {
99 const result: KdlNode[] = [];
100 while (cursor < tokens.length) {
101 if (is(tokens[cursor], "\n") || is(tokens[cursor], ";")) {
102 cursor++;
103 continue;
104 }
105 if (is(tokens[cursor], "}")) {
106 if (!nested) throw invalid();
107 return result;
108 }
109 const disabled = is(tokens[cursor], "/-");
110 if (disabled) cursor++;
111 const name = tokens[cursor++];
112 if (!name || ["{", "}", "=", ";", "\n", "("].includes(name.value)) throw invalid();
113 const node: KdlNode = {
114 name: name.value,
115 header: [name],
116 children: [],
117 start: name.start,
118 end: name.end,
119 close: undefined,
120 };
121 while (
122 cursor < tokens.length &&
123 !is(tokens[cursor], "\n") &&
124 !is(tokens[cursor], ";") &&
125 !is(tokens[cursor], "}")
126 ) {
127 const token = tokens[cursor++]!;
128 if (is(token, "{")) {
129 node.children = nodes(true);
130 const close = tokens[cursor++];
131 if (!is(close, "}")) throw invalid();
132 node.close = close!.start;
133 node.end = close!.end;
134 break;
135 }
136 node.header.push(token);
137 node.end = token.end;
138 }
139 if (!disabled) result.push(node);
140 }
141 if (nested) throw invalid();
142 return result;
143 };
144 return nodes(false);
145}