import * as Schema from "effect/Schema"; const decodeString = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.String)); /** A lossless structural reader: edits only binding nodes, never reformats a user's KDL. */type Token = { value: string; start: number; end: number; quoted: boolean };export type KdlNode = { name: string; header: Token[]; children: KdlNode[]; start: number; end: number; close: number | undefined;}; export function readKdlNodes(source: string): KdlNode[] { const tokens: Token[] = []; let offset = 0; const invalid = () => new Error("Couldn't read this Niri config. Use Advanced to configure it manually."); while (offset < source.length) { const start = offset; const rest = source.slice(offset); if (/^[\t \r\uFEFF\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/.test(rest)) { offset++; continue; } if (rest.startsWith("//")) { const end = source.indexOf("\n", offset); offset = end < 0 ? source.length : end; continue; } if (rest.startsWith("/*")) { offset += 2; let depth = 1; while (depth && offset < source.length) { if (source.startsWith("/*", offset)) { depth++; offset += 2; } else if (source.startsWith("*/", offset)) { depth--; offset += 2; } else offset++; } if (depth) throw invalid(); continue; } const continuation = /^\\[\t \r\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]*\n/.exec(rest); if (continuation) { offset += continuation[0].length; continue; } const raw = /^r(#{0,32})"/.exec(rest); if (raw) { const terminator = `"${raw[1]}`; const end = source.indexOf(terminator, offset + raw[0].length); if (end < 0) throw invalid(); offset = end + terminator.length; tokens.push({ value: source.slice(start + raw[0].length, end), start, end: offset, quoted: true, }); continue; } if (rest[0] === '"') { offset++; while (offset < source.length && source[offset] !== '"') { if (source[offset] === "\\") offset++; offset++; } if (offset === source.length) throw invalid(); offset++; // KDL's unicode escapes differ from JSON; names needing those stay on the manual path. let value: string; try { value = decodeString(source.slice(start, offset)); } catch { throw invalid(); } tokens.push({ value, start, end: offset, quoted: true }); continue; } if (rest.startsWith("/-")) offset += 2; else if ("{};\n=()".includes(rest[0]!)) offset++; else { while (offset < source.length && !/[\s{};="()]/.test(source[offset]!)) { if (["//", "/*", "/-"].some((prefix) => source.startsWith(prefix, offset))) break; offset++; } } if (offset === start) throw invalid(); tokens.push({ value: source.slice(start, offset), start, end: offset, quoted: false }); } let cursor = 0; const is = (token: Token | undefined, value: string) => token && !token.quoted && token.value === value; const nodes = (nested: boolean): KdlNode[] => { const result: KdlNode[] = []; while (cursor < tokens.length) { if (is(tokens[cursor], "\n") || is(tokens[cursor], ";")) { cursor++; continue; } if (is(tokens[cursor], "}")) { if (!nested) throw invalid(); return result; } const disabled = is(tokens[cursor], "/-"); if (disabled) cursor++; const name = tokens[cursor++]; if (!name || ["{", "}", "=", ";", "\n", "("].includes(name.value)) throw invalid(); const node: KdlNode = { name: name.value, header: [name], children: [], start: name.start, end: name.end, close: undefined, }; while ( cursor < tokens.length && !is(tokens[cursor], "\n") && !is(tokens[cursor], ";") && !is(tokens[cursor], "}") ) { const token = tokens[cursor++]!; if (is(token, "{")) { node.children = nodes(true); const close = tokens[cursor++]; if (!is(close, "}")) throw invalid(); node.close = close!.start; node.end = close!.end; break; } node.header.push(token); node.end = token.end; } if (!disabled) result.push(node); } if (nested) throw invalid(); return result; }; return nodes(false);}
import * as Schema from "effect/Schema";
const decodeString = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.String));
/** A lossless structural reader: edits only binding nodes, never reformats a user's KDL. */
type Token = { value: string; start: number; end: number; quoted: boolean };
export type KdlNode = {
name: string;
header: Token[];
children: KdlNode[];
start: number;
end: number;
close: number | undefined;
};
export function readKdlNodes(source: string): KdlNode[] {
const tokens: Token[] = [];
let offset = 0;
const invalid = () =>
new Error("Couldn't read this Niri config. Use Advanced to configure it manually.");
while (offset < source.length) {
const start = offset;
const rest = source.slice(offset);
if (/^[\t \r\uFEFF\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/.test(rest)) {
offset++;
continue;
}
if (rest.startsWith("//")) {
const end = source.indexOf("\n", offset);
offset = end < 0 ? source.length : end;
continue;
}
if (rest.startsWith("/*")) {
offset += 2;
let depth = 1;
while (depth && offset < source.length) {
if (source.startsWith("/*", offset)) {
depth++;
offset += 2;
} else if (source.startsWith("*/", offset)) {
depth--;
offset += 2;
} else offset++;
}
if (depth) throw invalid();
continue;
}
const continuation = /^\\[\t \r\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]*\n/.exec(rest);
if (continuation) {
offset += continuation[0].length;
continue;
}
const raw = /^r(#{0,32})"/.exec(rest);
if (raw) {
const terminator = `"${raw[1]}`;
const end = source.indexOf(terminator, offset + raw[0].length);
if (end < 0) throw invalid();
offset = end + terminator.length;
tokens.push({
value: source.slice(start + raw[0].length, end),
start,
end: offset,
quoted: true,
});
continue;
}
if (rest[0] === '"') {
offset++;
while (offset < source.length && source[offset] !== '"') {
if (source[offset] === "\\") offset++;
offset++;
}
if (offset === source.length) throw invalid();
offset++;
// KDL's unicode escapes differ from JSON; names needing those stay on the manual path.
let value: string;
try {
value = decodeString(source.slice(start, offset));
} catch {
throw invalid();
}
tokens.push({ value, start, end: offset, quoted: true });
continue;
}
if (rest.startsWith("/-")) offset += 2;
else if ("{};\n=()".includes(rest[0]!)) offset++;
else {
while (offset < source.length && !/[\s{};="()]/.test(source[offset]!)) {
if (["//", "/*", "/-"].some((prefix) => source.startsWith(prefix, offset))) break;
offset++;
}
}
if (offset === start) throw invalid();
tokens.push({ value: source.slice(start, offset), start, end: offset, quoted: false });
}
let cursor = 0;
const is = (token: Token | undefined, value: string) =>
token && !token.quoted && token.value === value;
const nodes = (nested: boolean): KdlNode[] => {
const result: KdlNode[] = [];
while (cursor < tokens.length) {
if (is(tokens[cursor], "\n") || is(tokens[cursor], ";")) {
cursor++;
continue;
}
if (is(tokens[cursor], "}")) {
if (!nested) throw invalid();
return result;
}
const disabled = is(tokens[cursor], "/-");
if (disabled) cursor++;
const name = tokens[cursor++];
if (!name || ["{", "}", "=", ";", "\n", "("].includes(name.value)) throw invalid();
const node: KdlNode = {
name: name.value,
header: [name],
children: [],
start: name.start,
end: name.end,
close: undefined,
};
while (
cursor < tokens.length &&
!is(tokens[cursor], "\n") &&
!is(tokens[cursor], ";") &&
!is(tokens[cursor], "}")
) {
const token = tokens[cursor++]!;
if (is(token, "{")) {
node.children = nodes(true);
const close = tokens[cursor++];
if (!is(close, "}")) throw invalid();
node.close = close!.start;
node.end = close!.end;
break;
}
node.header.push(token);
node.end = token.end;
}
if (!disabled) result.push(node);
}
if (nested) throw invalid();
return result;
};
return nodes(false);
}