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