1"""Indexes of closing delimiters for one inline source.
2
3Each delimiter family scans its source once. These are lexical positions, not
4memoized parse failures, and are never shared with copied or sliced sources.
5"""
6
7import re
8from bisect import bisect_left, bisect_right
9from typing import Dict, List, Optional, Tuple
10
11_BACKTICKS = re.compile(r"`+")
12
13
14class DelimiterScanner:
15 def __init__(self, src: str) -> None:
16 self.src = src
17 self._code: Optional[Dict[int, List[int]]] = None
18 self._ends: Dict[Tuple[str, bool], List[int]] = {}
19
20 def code_end(self, marker: str, pos: int) -> Optional[int]:
21 size = len(marker)
22 if self._code is None:
23 end = self.src.find(marker, pos)
24 if end >= 0 and (end == 0 or self.src[end - 1] != "`"):
25 after = end + size
26 if after == len(self.src) or self.src[after] != "`":
27 return end
28 self._code = {}
29 for match in _BACKTICKS.finditer(self.src):
30 self._code.setdefault(match.end() - match.start(), []).append(match.start())
31 positions = self._code.get(size, [])
32 index = bisect_left(positions, pos)
33 return positions[index] if index < len(positions) else None
34
35 def find(self, marker: str, pos: int, *, formatting: bool = False) -> Optional[int]:
36 key = (marker, formatting)
37 positions = self._ends.get(key)
38 if positions is None:
39 positions = []
40 start = self.src.find(marker)
41 while start >= 0:
42 if not formatting or _formatting_closer(self.src, start, marker):
43 positions.append(start)
44 start = self.src.find(marker, start + 1)
45 self._ends[key] = positions
46 index = bisect_right(positions, pos) if formatting else bisect_left(positions, pos)
47 return positions[index] if index < len(positions) else None
48
49
50def _formatting_closer(src: str, start: int, marker: str) -> bool:
51 char = marker[0]
52 end = start + len(marker)
53 if end < len(src) and src[end] == char and not src.startswith(marker, end):
54 return False
55 if not start:
56 return False
57 previous = src[start - 1]
58 if previous != char:
59 return not previous.isspace()
60 # A literal escaped first character can precede a closing pair in a run.
61 if start < 2 or src[start - 2] != "\\":
62 return False
63 backslashes = 0
64 pos = start - 2
65 while pos >= 0 and src[pos] == "\\":
66 backslashes += 1
67 pos -= 1
68 return backslashes % 2 == 1