146 lines · 4.5 KB · TypeScript
RawDownload
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);}