1const LEFT_BRACKETS = ['(', '{'] as const;
2const RIGHT_BRACKETS = [')', '}'] as const;
3
4type LeftBracket = (typeof LEFT_BRACKETS)[number];
5type RightBracket = (typeof RIGHT_BRACKETS)[number];
6type Bracket = LeftBracket | RightBracket;
7
8export function findMatchingBracketPosition(
9  contents: string,
10  bracket: Bracket,
11  offset: number = 0
12): number {
13  // search first occurrence of `bracket`
14  const firstBracketPos = contents.indexOf(bracket, offset);
15  if (firstBracketPos < 0) {
16    return -1;
17  }
18
19  let stackCounter = 0;
20  const matchingBracket = getMatchingBracket(bracket);
21
22  if (isLeftBracket(bracket)) {
23    const contentsLength = contents.length;
24    // search forward
25    for (let i = firstBracketPos + 1; i < contentsLength; ++i) {
26      const c = contents[i];
27      if (c === bracket) {
28        stackCounter += 1;
29      } else if (c === matchingBracket) {
30        if (stackCounter === 0) {
31          return i;
32        }
33        stackCounter -= 1;
34      }
35    }
36  } else {
37    // search backward
38    for (let i = firstBracketPos - 1; i >= 0; --i) {
39      const c = contents[i];
40      if (c === bracket) {
41        stackCounter += 1;
42      } else if (c === matchingBracket) {
43        if (stackCounter === 0) {
44          return i;
45        }
46        stackCounter -= 1;
47      }
48    }
49  }
50
51  return -1;
52}
53
54function isLeftBracket(bracket: Bracket): boolean {
55  const leftBracketList: readonly Bracket[] = LEFT_BRACKETS;
56  return leftBracketList.includes(bracket);
57}
58
59function getMatchingBracket(bracket: Bracket): Bracket {
60  switch (bracket) {
61    case '(':
62      return ')';
63    case ')':
64      return '(';
65    case '{':
66      return '}';
67    case '}':
68      return '{';
69    default:
70      throw new Error(`Unsupported bracket - ${bracket}`);
71  }
72}
73