1082815dcSEvan Baconconst LEFT_BRACKETS = ['(', '{'] as const;
2082815dcSEvan Baconconst RIGHT_BRACKETS = [')', '}'] as const;
3082815dcSEvan Bacon
4*8a424bebSJames Idetype LeftBracket = (typeof LEFT_BRACKETS)[number];
5*8a424bebSJames Idetype RightBracket = (typeof RIGHT_BRACKETS)[number];
6082815dcSEvan Bacontype Bracket = LeftBracket | RightBracket;
7082815dcSEvan Bacon
8082815dcSEvan Baconexport function findMatchingBracketPosition(
9082815dcSEvan Bacon  contents: string,
10082815dcSEvan Bacon  bracket: Bracket,
11082815dcSEvan Bacon  offset: number = 0
12082815dcSEvan Bacon): number {
13082815dcSEvan Bacon  // search first occurrence of `bracket`
14082815dcSEvan Bacon  const firstBracketPos = contents.indexOf(bracket, offset);
15082815dcSEvan Bacon  if (firstBracketPos < 0) {
16082815dcSEvan Bacon    return -1;
17082815dcSEvan Bacon  }
18082815dcSEvan Bacon
19082815dcSEvan Bacon  let stackCounter = 0;
20082815dcSEvan Bacon  const matchingBracket = getMatchingBracket(bracket);
21082815dcSEvan Bacon
22082815dcSEvan Bacon  if (isLeftBracket(bracket)) {
23082815dcSEvan Bacon    const contentsLength = contents.length;
24082815dcSEvan Bacon    // search forward
25082815dcSEvan Bacon    for (let i = firstBracketPos + 1; i < contentsLength; ++i) {
26082815dcSEvan Bacon      const c = contents[i];
27082815dcSEvan Bacon      if (c === bracket) {
28082815dcSEvan Bacon        stackCounter += 1;
29082815dcSEvan Bacon      } else if (c === matchingBracket) {
30082815dcSEvan Bacon        if (stackCounter === 0) {
31082815dcSEvan Bacon          return i;
32082815dcSEvan Bacon        }
33082815dcSEvan Bacon        stackCounter -= 1;
34082815dcSEvan Bacon      }
35082815dcSEvan Bacon    }
36082815dcSEvan Bacon  } else {
37082815dcSEvan Bacon    // search backward
38082815dcSEvan Bacon    for (let i = firstBracketPos - 1; i >= 0; --i) {
39082815dcSEvan Bacon      const c = contents[i];
40082815dcSEvan Bacon      if (c === bracket) {
41082815dcSEvan Bacon        stackCounter += 1;
42082815dcSEvan Bacon      } else if (c === matchingBracket) {
43082815dcSEvan Bacon        if (stackCounter === 0) {
44082815dcSEvan Bacon          return i;
45082815dcSEvan Bacon        }
46082815dcSEvan Bacon        stackCounter -= 1;
47082815dcSEvan Bacon      }
48082815dcSEvan Bacon    }
49082815dcSEvan Bacon  }
50082815dcSEvan Bacon
51082815dcSEvan Bacon  return -1;
52082815dcSEvan Bacon}
53082815dcSEvan Bacon
54082815dcSEvan Baconfunction isLeftBracket(bracket: Bracket): boolean {
55082815dcSEvan Bacon  const leftBracketList: readonly Bracket[] = LEFT_BRACKETS;
56082815dcSEvan Bacon  return leftBracketList.includes(bracket);
57082815dcSEvan Bacon}
58082815dcSEvan Bacon
59082815dcSEvan Baconfunction getMatchingBracket(bracket: Bracket): Bracket {
60082815dcSEvan Bacon  switch (bracket) {
61082815dcSEvan Bacon    case '(':
62082815dcSEvan Bacon      return ')';
63082815dcSEvan Bacon    case ')':
64082815dcSEvan Bacon      return '(';
65082815dcSEvan Bacon    case '{':
66082815dcSEvan Bacon      return '}';
67082815dcSEvan Bacon    case '}':
68082815dcSEvan Bacon      return '{';
69082815dcSEvan Bacon    default:
70082815dcSEvan Bacon      throw new Error(`Unsupported bracket - ${bracket}`);
71082815dcSEvan Bacon  }
72082815dcSEvan Bacon}
73