backrefs 8.1__py315-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. backrefs/__init__.py +2 -0
  2. backrefs/__meta__.py +197 -0
  3. backrefs/_bre_parse.py +1494 -0
  4. backrefs/_bregex_parse.py +1332 -0
  5. backrefs/_bregex_typing.py +6 -0
  6. backrefs/bre.py +656 -0
  7. backrefs/bregex.py +730 -0
  8. backrefs/py.typed +0 -0
  9. backrefs/uniprops/__init__.py +665 -0
  10. backrefs/uniprops/unidata/__init__.py +1 -0
  11. backrefs/uniprops/unidata/age.py +123 -0
  12. backrefs/uniprops/unidata/alias.py +796 -0
  13. backrefs/uniprops/unidata/bidiclass.py +99 -0
  14. backrefs/uniprops/unidata/bidipairedbrackettype.py +19 -0
  15. backrefs/uniprops/unidata/binary.py +331 -0
  16. backrefs/uniprops/unidata/block.py +1395 -0
  17. backrefs/uniprops/unidata/canonicalcombiningclass.py +1031 -0
  18. backrefs/uniprops/unidata/decompositiontype.py +79 -0
  19. backrefs/uniprops/unidata/eastasianwidth.py +31 -0
  20. backrefs/uniprops/unidata/generalcategory.py +173 -0
  21. backrefs/uniprops/unidata/graphemeclusterbreak.py +79 -0
  22. backrefs/uniprops/unidata/hangulsyllabletype.py +31 -0
  23. backrefs/uniprops/unidata/indicpositionalcategory.py +71 -0
  24. backrefs/uniprops/unidata/indicsyllabiccategory.py +155 -0
  25. backrefs/uniprops/unidata/joininggroup.py +431 -0
  26. backrefs/uniprops/unidata/joiningtype.py +31 -0
  27. backrefs/uniprops/unidata/linebreak.py +203 -0
  28. backrefs/uniprops/unidata/numerictype.py +23 -0
  29. backrefs/uniprops/unidata/numericvalue.py +583 -0
  30. backrefs/uniprops/unidata/quickcheck.py +59 -0
  31. backrefs/uniprops/unidata/script.py +711 -0
  32. backrefs/uniprops/unidata/scriptextensions.py +711 -0
  33. backrefs/uniprops/unidata/sentencebreak.py +67 -0
  34. backrefs/uniprops/unidata/verticalorientation.py +23 -0
  35. backrefs/uniprops/unidata/wordbreak.py +99 -0
  36. backrefs/util.py +167 -0
  37. backrefs-8.1.dist-info/METADATA +70 -0
  38. backrefs-8.1.dist-info/RECORD +40 -0
  39. backrefs-8.1.dist-info/WHEEL +4 -0
  40. backrefs-8.1.dist-info/licenses/LICENSE.md +21 -0
backrefs/_bre_parse.py ADDED
@@ -0,0 +1,1494 @@
1
+ """
2
+ Backrefs Re parser.
3
+
4
+ Licensed under MIT
5
+ Copyright (c) 2011 - 2020 Isaac Muse <isaacmuse@gmail.com>
6
+ """
7
+ from __future__ import annotations
8
+ import re as _re
9
+ import sys
10
+ import copyreg as _copyreg
11
+ from . import util as _util
12
+ from .util import PatternError
13
+ import unicodedata as _unicodedata
14
+ from . import uniprops as _uniprops
15
+ from typing import Generic, AnyStr, Match, Any, Pattern, cast
16
+
17
+ if sys.version_info >= (3, 11):
18
+ import re._parser as _parser # type: ignore[import]
19
+ else:
20
+ import sre_parse as _parser
21
+
22
+ __all__ = ("ReplaceTemplate", "PatternError")
23
+
24
+ _ASCII_LETTERS = frozenset(
25
+ (
26
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
27
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
28
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
29
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
30
+ )
31
+ )
32
+ _DIGIT = frozenset(('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'))
33
+ _OCTAL = frozenset(('0', '1', '2', '3', '4', '5', '6', '7'))
34
+ _HEX = frozenset(('a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'))
35
+ _LETTERS_UNDERSCORE = _ASCII_LETTERS | frozenset(('_',))
36
+ _WORD = _LETTERS_UNDERSCORE | _DIGIT
37
+ _STANDARD_ESCAPES = frozenset(('a', 'b', 'f', 'n', 'r', 't', 'v'))
38
+ _CURLY_BRACKETS = frozenset(('{', '}'))
39
+ _PROPERTY_STRIP = frozenset((' ', '-', '_'))
40
+ _PROPERTY = _WORD | _DIGIT | _PROPERTY_STRIP
41
+ _SCOPED_FLAGS = frozenset(('a', 'i', 'L', 'm', 'u', 's', 'x'))
42
+ _SCOPED_INV_FLAGS = frozenset(('i', 'm', 's', 'x'))
43
+ _SCOPED_END = frozenset((':', ')'))
44
+
45
+ _CURLY_BRACKETS_ORD = frozenset((0x7b, 0x7d))
46
+
47
+ _COMPATIBILITY_PROPERTIES = frozenset(
48
+ (
49
+ 'alpha', 'lower', 'upper', 'punct', 'digit', 'xdigit', 'alnum',
50
+ 'space', 'blank', 'cntrl', 'graph', 'print', 'word'
51
+ )
52
+ )
53
+
54
+ # Case upper or lower
55
+ _UPPER = 1
56
+ _LOWER = 2
57
+
58
+ # Format Constants
59
+ _BACK_SLASH_TRANSLATION = {
60
+ "\\a": '\a',
61
+ "\\b": '\b',
62
+ "\\f": '\f',
63
+ "\\r": '\r',
64
+ "\\t": '\t',
65
+ "\\n": '\n',
66
+ "\\v": '\v',
67
+ "\\\\": '\\'
68
+ }
69
+
70
+ _FMT_CONV_TYPE = ('a', 'r', 's')
71
+
72
+
73
+ class _SearchParser(Generic[AnyStr]):
74
+ """Search Template."""
75
+
76
+ _new_refs = ("c", "C", "e", "E", "h", "l", "L", "m", "M", "N", "p", "P", "Q", "R", "X")
77
+ _re_start_wb = r"\b(?=\w)"
78
+ _re_end_wb = r"\b(?<=\w)"
79
+ _line_break = r'(?:\r\n|(?!\r\n)[\n\v\f\r\x85\u2028\u2029])'
80
+ _bytes_line_break = r'(?>\r\n|[\n\v\f\r\x85])' if _util.PY311 else r'(?:\r\n|(?!\r\n)[\n\v\f\r\x85])'
81
+ _grapheme_cluster = r'(?:{}{}*(?!{}))'
82
+
83
+ verbose: bool
84
+ unicode: bool
85
+ ascii: bool # noqa: A003
86
+ is_bytes: bool
87
+ search: AnyStr
88
+
89
+ def __init__(self, search: AnyStr, re_verbose: bool = False, re_unicode: bool | None = None) -> None:
90
+ """Initialize."""
91
+
92
+ if isinstance(search, bytes):
93
+ self.is_bytes = True
94
+ else:
95
+ self.is_bytes = False
96
+
97
+ if self.is_bytes:
98
+ self._re_line_break = self._bytes_line_break
99
+ else:
100
+ self._re_line_break = self._line_break
101
+ self.search = search
102
+ self.re_verbose = re_verbose
103
+ self.re_unicode = re_unicode
104
+
105
+ def process_quotes(self, text: str) -> str:
106
+ """Process quotes."""
107
+
108
+ escaped = False
109
+ in_quotes = False
110
+ current = []
111
+ quoted = [] # type: list[str]
112
+ i = _util.StringIter(text)
113
+
114
+ for t in i:
115
+ if not escaped and t == "\\":
116
+ escaped = True
117
+ elif escaped:
118
+ escaped = False
119
+ if t == "E":
120
+ if in_quotes:
121
+ current.append(_re.escape("".join(quoted)))
122
+ quoted = []
123
+ in_quotes = False
124
+ elif t == "Q" and not in_quotes:
125
+ in_quotes = True
126
+ elif in_quotes:
127
+ quoted.extend(["\\", t])
128
+ else:
129
+ current.extend(["\\", t])
130
+ elif in_quotes:
131
+ quoted.extend(t)
132
+ else:
133
+ current.append(t)
134
+
135
+ if in_quotes and escaped:
136
+ quoted.append("\\")
137
+ elif escaped:
138
+ current.append("\\")
139
+
140
+ if quoted:
141
+ current.append(_re.escape("".join(quoted)))
142
+
143
+ return "".join(current)
144
+
145
+ def verbose_comment(self, t: str, i: _util.StringIter) -> list[str]:
146
+ """Handle verbose comments."""
147
+
148
+ current = []
149
+ escaped = False
150
+
151
+ try:
152
+ while t != "\n":
153
+ if not escaped and t == "\\":
154
+ escaped = True
155
+ current.append(t)
156
+ elif escaped:
157
+ escaped = False
158
+ if t in self._new_refs:
159
+ current.append("\\")
160
+ current.append(t)
161
+ else:
162
+ current.append(t)
163
+ t = next(i)
164
+ except StopIteration:
165
+ pass
166
+
167
+ if t == "\n":
168
+ current.append(t)
169
+ return current
170
+
171
+ def get_unicode_property(self, i: _util.StringIter, brackets: bool = False) -> tuple[str, str]:
172
+ """Get Unicode property."""
173
+
174
+ index = i.index
175
+ prop = []
176
+ value = []
177
+ try:
178
+ c = next(i)
179
+ if c.upper() in _ASCII_LETTERS:
180
+ prop.append(c)
181
+ elif (not brackets and c != '{') or (brackets and c != ':'):
182
+ raise PatternError(f"Unicode property missing '{{' at {i.index - 1}!")
183
+ else:
184
+ c = next(i)
185
+ if c == '^':
186
+ prop.append(c)
187
+ c = next(i)
188
+
189
+ while c not in (':', '=', '}'):
190
+ if c not in _PROPERTY:
191
+ raise PatternError(f'Invalid Unicode property character at {i.index - 1}!')
192
+ if c not in _PROPERTY_STRIP:
193
+ prop.append(c)
194
+ c = next(i)
195
+
196
+ if c in (':', '='):
197
+ skip = False
198
+ if brackets:
199
+ is_colon = c == ':'
200
+ c = next(i)
201
+ if is_colon and c == ']':
202
+ # That's the end of the property
203
+ skip = True
204
+ end = ':'
205
+ else:
206
+ c = next(i)
207
+ end = '}'
208
+
209
+ # Get the property value
210
+ if not skip:
211
+ while c != end:
212
+ if c not in _PROPERTY:
213
+ raise PatternError(f'Invalid Unicode property character at {i.index - 1}!')
214
+ if c not in _PROPERTY_STRIP:
215
+ value.append(c)
216
+ c = next(i)
217
+ if brackets and c == ':':
218
+ c = next(i)
219
+ if c != ']':
220
+ raise PatternError(f'Invalid Unicode property character at {i.index - 1}!')
221
+ if not value:
222
+ raise PatternError('Invalid Unicode property!')
223
+
224
+ except StopIteration as e:
225
+ if brackets:
226
+ raise PatternError(f"Missing or unmatched ':]' at {index}!") from e
227
+ else:
228
+ raise PatternError(f"Missing or unmatched '{{' at {index}!") from e
229
+
230
+ p = ''.join(prop).lower()
231
+ v = ''.join(value).lower()
232
+
233
+ # Ensure when using POSIX form, that any property considered a compatibility property uses the POSIX form.
234
+ # POSIX form is not guaranteed to be different from standard form and sometimes is just an alias for standard.
235
+ if brackets and p in _COMPATIBILITY_PROPERTIES:
236
+ p = 'posix' + p
237
+
238
+ return p, v
239
+
240
+ def get_named_unicode(self, i: _util.StringIter) -> str:
241
+ """Get Unicode name."""
242
+
243
+ index = i.index
244
+ value = []
245
+ try:
246
+ if next(i) != '{':
247
+ raise PatternError(f"Named Unicode missing '{{' {i.index - 1}!")
248
+ c = next(i)
249
+ while c != '}':
250
+ value.append(c)
251
+ c = next(i)
252
+ except Exception as e:
253
+ raise PatternError(f"Unmatched '{{' at {index}!") from e
254
+
255
+ return ''.join(value)
256
+
257
+ def reference(self, t: str, i: _util.StringIter, in_group: bool = False) -> list[str]:
258
+ """Handle references."""
259
+
260
+ current = []
261
+
262
+ if not in_group and t == "m":
263
+ current.append(self._re_start_wb)
264
+ elif not in_group and t == "M":
265
+ current.append(self._re_end_wb)
266
+ elif not in_group and t == "R":
267
+ current.append(self._re_line_break)
268
+ elif not in_group and t == "X":
269
+ no_mark = self.unicode_props("^m", None, in_group=False)[0]
270
+ mark = self.unicode_props("m", None, in_group=False)[0]
271
+ current.extend(self._grapheme_cluster.format(no_mark, mark, mark))
272
+ elif t == 'p':
273
+ prop = self.get_unicode_property(i)
274
+ current.extend(self.unicode_props(prop[0], prop[1], in_group=in_group))
275
+ if in_group:
276
+ self.found_property = True
277
+ elif t == 'P':
278
+ prop = self.get_unicode_property(i)
279
+ current.extend(self.unicode_props(prop[0], prop[1], in_group=in_group, negate=True))
280
+ if in_group:
281
+ self.found_property = True
282
+ elif t == "N":
283
+ text = self.get_named_unicode(i)
284
+ current.extend(self.unicode_name(text, in_group))
285
+ if in_group:
286
+ self.found_named_unicode = True
287
+ else:
288
+ current.extend(["\\", t])
289
+ return current
290
+
291
+ def get_comments(self, i: _util.StringIter) -> str | None:
292
+ """Get comments."""
293
+
294
+ index = i.index
295
+ value = ['(']
296
+ escaped = False
297
+ try:
298
+ c = next(i)
299
+ if c != '?':
300
+ i.rewind(1)
301
+ return None
302
+ value.append(c)
303
+ c = next(i)
304
+ if c != '#':
305
+ i.rewind(2)
306
+ return None
307
+ value.append(c)
308
+ c = next(i)
309
+ while c != ')' or escaped is True:
310
+ if escaped:
311
+ escaped = False
312
+ elif c == '\\':
313
+ escaped = True
314
+ value.append(c)
315
+ c = next(i)
316
+ value.append(c)
317
+ except StopIteration as e:
318
+ raise PatternError(f"Unmatched '(' at {index - 1}!") from e
319
+
320
+ return ''.join(value)
321
+
322
+ def flags(self, text: str, scoped: bool = False) -> None:
323
+ """Analyze flags."""
324
+
325
+ flags = text.split('-')
326
+ enable = flags[0]
327
+ disable = flags[1] if len(flags) > 1 else ''
328
+
329
+ if ('a' in enable or 'L' in enable) and self.unicode:
330
+ self.unicode = False
331
+ elif 'u' in enable and not self.unicode and not self.is_bytes:
332
+ self.unicode = True
333
+ if 'x' in disable and self.verbose:
334
+ self.verbose = False
335
+ elif 'x' in enable and not self.verbose:
336
+ self.verbose = True
337
+
338
+ def get_flags(self, i: _util.StringIter) -> tuple[str | None, bool]:
339
+ """
340
+ Get flags.
341
+
342
+ In Re, flags are quite predictable when global or scoped.
343
+ Global can never be disabled with minus, and never have a `:` after them.
344
+ The global flag set is also very specific, but can be used as enablers in scoped.
345
+
346
+ The returned scoped status will indicate whether flags are generally considered
347
+ scoped flags or global flags.
348
+ """
349
+
350
+ index = i.index
351
+ value = ['(']
352
+ smells_scoped = False
353
+ try:
354
+ c = next(i)
355
+ if c != '?':
356
+ i.rewind(1)
357
+ return None, False
358
+ value.append(c)
359
+ c = next(i)
360
+ while c not in _SCOPED_END:
361
+ if c == '-':
362
+ c2 = next(i)
363
+ if c2 not in _SCOPED_INV_FLAGS:
364
+ raise PatternError('Bad scope')
365
+ c += c2
366
+ smells_scoped = True
367
+ elif c not in _SCOPED_FLAGS:
368
+ raise PatternError("Bad flag")
369
+ value.append(c)
370
+ c = next(i)
371
+ if smells_scoped and c != ':':
372
+ raise PatternError("Bad flag")
373
+ elif c == ':':
374
+ smells_scoped = True
375
+ value.append(c)
376
+ except Exception:
377
+ i.rewind(i.index - index)
378
+ value = []
379
+
380
+ return ''.join(value) if value else None, smells_scoped
381
+
382
+ def subgroup(self, t: str, i: _util.StringIter) -> list[str]:
383
+ """Handle parenthesis."""
384
+
385
+ current = [] # type: list[str]
386
+
387
+ # (?#comment)
388
+ comments = self.get_comments(i)
389
+ if comments:
390
+ return [comments]
391
+
392
+ verbose = self.verbose
393
+ unicode_flag = self.unicode
394
+ pos = i.index - 1
395
+
396
+ # (?flags:pattern) or (?flags)
397
+ flags, scoped = self.get_flags(i)
398
+ if flags and flags != '(?)':
399
+ t = flags
400
+ self.flags(flags[2:-1], scoped=scoped)
401
+ if not scoped:
402
+ if pos != 0:
403
+ raise PatternError(f'Global flags not at the start of the expression at position {pos}')
404
+ return [flags]
405
+
406
+ current = []
407
+ try:
408
+ while t != ')':
409
+ if not current:
410
+ current.append(t)
411
+ else:
412
+ current.extend(self.normal(t, i))
413
+
414
+ t = next(i)
415
+ except StopIteration as e:
416
+ raise PatternError(f"Missing ')', unterminated subpattern at position {pos}") from e
417
+
418
+ # Restore flags after group
419
+ self.verbose = verbose
420
+ self.unicode = unicode_flag
421
+
422
+ if t == ")":
423
+ current.append(t)
424
+ return current
425
+
426
+ def char_groups(self, t: str, i: _util.StringIter) -> list[str]:
427
+ """Handle character groups."""
428
+
429
+ current = []
430
+ pos = i.index - 1
431
+ found = False
432
+ escaped = False
433
+ first = 0
434
+ found_property = False
435
+ self.found_property = False
436
+ self.found_named_unicode = False
437
+
438
+ try:
439
+ while True:
440
+ # Prevent POSIX/Unicode class from being part of a range.
441
+ if self.found_property and t == '-':
442
+ current.append(_re.escape(t))
443
+ pos += 1
444
+ t = next(i)
445
+ self.found_property = False
446
+ continue
447
+ else:
448
+ self.found_property = False
449
+
450
+ if not escaped and t == "\\":
451
+ escaped = True
452
+ elif escaped:
453
+ escaped = False
454
+ idx = len(current) - 1
455
+ current.extend(self.reference(t, i, True))
456
+ if self.found_property:
457
+ # Prevent Unicode class from being part of a range.
458
+ if idx >= 0 and current[idx] == '-':
459
+ current[idx] = _re.escape('-')
460
+ found_property = True
461
+ elif t == "[" and not found:
462
+ found = True
463
+ first = pos
464
+ current.append(t)
465
+ elif t == "[":
466
+ index = i.index
467
+ try:
468
+ prop = self.get_unicode_property(i, True) # type: tuple[str, str] | None
469
+ except Exception:
470
+ prop = None
471
+ i.rewind(i.index - index)
472
+ if prop is not None:
473
+ value = self.unicode_props(prop[0], prop[1], in_group=True)
474
+ if current[-1] == '-':
475
+ current[-1] = _re.escape('-')
476
+ current.extend(value)
477
+ found_property = True
478
+ pos = i.index - 2
479
+ else:
480
+ current.append(t)
481
+ elif t == "^" and found and (pos == first + 1):
482
+ first = pos
483
+ current.append(t)
484
+ elif t == "]" and found and (pos != first + 1):
485
+ found = False
486
+ current.append(t)
487
+ break
488
+ else:
489
+ current.append(t)
490
+ pos += 1
491
+ t = next(i)
492
+ except StopIteration as e:
493
+ raise PatternError(f"Missing ']', unterminated character set at position {pos}") from e
494
+
495
+ # Handle properties that return an empty string.
496
+ # This will occur when a property's values exceed
497
+ # either the Unicode char limit on a narrow system,
498
+ # or the ASCII limit in a byte string pattern.
499
+ if found_property or self.found_named_unicode:
500
+ temp = "".join(current)
501
+ if temp == '[]':
502
+ # We specified some properties, but they are all
503
+ # out of reach. Therefore we can match nothing.
504
+ current = ['(?!)']
505
+ elif temp == '[^]':
506
+ current = [f'[{_uniprops.ASCII_RANGE if self.is_bytes else _uniprops.UNICODE_RANGE}]']
507
+ else:
508
+ current = [temp]
509
+
510
+ return current
511
+
512
+ def normal(self, t: str, i: _util.StringIter) -> list[str]:
513
+ """Handle normal chars."""
514
+
515
+ current = []
516
+
517
+ if t == "\\":
518
+ try:
519
+ t = next(i)
520
+ current.extend(self.reference(t, i))
521
+ except StopIteration:
522
+ current.append(t)
523
+ elif t == "(":
524
+ current.extend(self.subgroup(t, i))
525
+ elif self.verbose and t == "#":
526
+ current.extend(self.verbose_comment(t, i))
527
+ elif t == "[":
528
+ current.extend(self.char_groups(t, i))
529
+ else:
530
+ current.append(t)
531
+ return current
532
+
533
+ def unicode_name(self, name: str, in_group: bool = False) -> list[str]:
534
+ """Insert Unicode value by its name."""
535
+
536
+ value = ord(_unicodedata.lookup(name))
537
+ if self.is_bytes and value > 0xFF:
538
+ if not in_group:
539
+ return ['(?!)']
540
+ else:
541
+ return ['']
542
+ return [f'\\{value:03o}' if value <= 0xFF else chr(value)]
543
+
544
+ def unicode_props(
545
+ self,
546
+ props: str,
547
+ prop_value: str | None,
548
+ in_group: bool = False,
549
+ negate: bool = False
550
+ ) -> list[str]:
551
+ """
552
+ Insert Unicode properties.
553
+
554
+ Unicode properties are very forgiving.
555
+ Case doesn't matter and `[ -_]` will be stripped out.
556
+ """
557
+
558
+ if props.startswith("^"):
559
+ if negate:
560
+ props = props[1:]
561
+ elif negate:
562
+ props = '^' + props
563
+ if not prop_value and prop_value is not None:
564
+ prop_value = None
565
+
566
+ if self.is_bytes:
567
+ mode = _uniprops.MODE_ASCII
568
+ elif not self.unicode:
569
+ mode = _uniprops.MODE_NORMAL
570
+ else:
571
+ mode = _uniprops.MODE_UNICODE
572
+
573
+ v = _uniprops.get_unicode_property(props, prop_value, mode)
574
+ if not in_group:
575
+ if not v:
576
+ v = f'^{_uniprops.ASCII_RANGE if self.is_bytes else _uniprops.UNICODE_RANGE}'
577
+ v = f"[{v}]"
578
+ properties = [v]
579
+
580
+ return properties
581
+
582
+ def main_group(self, i: _util.StringIter) -> list[str]:
583
+ """The main group: group 0."""
584
+
585
+ current = []
586
+ try:
587
+ while True:
588
+ t = next(i)
589
+ current.extend(self.normal(t, i))
590
+ except StopIteration:
591
+ pass
592
+ return current
593
+
594
+ def _parse(self, search: str) -> str:
595
+ """Begin parsing."""
596
+
597
+ self.verbose = bool(self.re_verbose)
598
+ self.unicode = bool(self.re_unicode)
599
+ self.ascii = self.re_unicode is not None and not self.re_unicode
600
+ if not self.unicode and not self.ascii:
601
+ self.unicode = True
602
+
603
+ new_pattern = []
604
+ i = _util.StringIter(self.process_quotes(search))
605
+
606
+ new_pattern = self.main_group(i)
607
+ return "".join(new_pattern)
608
+
609
+ def parse(self) -> AnyStr:
610
+ """Apply search template."""
611
+
612
+ if isinstance(self.search, bytes):
613
+ return self._parse(self.search.decode('latin-1')).encode('latin-1')
614
+ else:
615
+ return self._parse(self.search)
616
+
617
+
618
+ class _ReplaceParser(Generic[AnyStr]):
619
+ """Pre-replace template."""
620
+
621
+ def __init__(self, pattern: Pattern[AnyStr], template: AnyStr, use_format: bool = False) -> None:
622
+ """Initialize."""
623
+
624
+ self.pattern = pattern # type: Pattern[AnyStr]
625
+ self._original = template # type: AnyStr
626
+ self._template = template # type: AnyStr
627
+ self.use_format = use_format
628
+ self.end_found = False
629
+ self.group_slots = [] # type: list[tuple[int, tuple[int | None, int | None, Any]]]
630
+ self.literal_slots = [] # type: list[str]
631
+ self.result = [] # type: list[str]
632
+ self.span_stack = [] # type: list[int]
633
+ self.single_stack = [] # type: list[int]
634
+ self.literals = [] # type: list[AnyStr | None]
635
+ self.groups = [] # type: list[tuple[int, int]]
636
+ self.slot = 0
637
+ self.manual = False
638
+ self.auto = False
639
+ self.auto_index = 0
640
+ self.is_bytes = isinstance(self._original, bytes)
641
+
642
+ def parse_format_index(self, text: str) -> int | str:
643
+ """Parse format index."""
644
+
645
+ base = 10
646
+ prefix = text[1:3] if text[0] == "-" else text[:2]
647
+ if prefix[0:1] == "0":
648
+ char = prefix[-1]
649
+ if char == "b":
650
+ base = 2
651
+ elif char == "o":
652
+ base = 8
653
+ elif char == "x":
654
+ base = 16
655
+ try:
656
+ idx = int(text, base) # type: int | str
657
+ except Exception:
658
+ idx = text
659
+ return idx
660
+
661
+ def get_format(self, c: str, i: _util.StringIter) -> tuple[str, list[tuple[int, Any]]]:
662
+ """Get format group."""
663
+
664
+ index = i.index
665
+ field = ''
666
+ value = [] # type: list[tuple[int, Any]]
667
+
668
+ try:
669
+ if c == '}':
670
+ value.append((_util.FMT_FIELD, ''))
671
+ value.append((_util.FMT_INDEX, -1))
672
+ else:
673
+ # Field
674
+ temp = [] # type: list[str]
675
+ if c in _LETTERS_UNDERSCORE:
676
+ # Handle name
677
+ temp.append(c)
678
+ c = self.format_next(i)
679
+ while c in _WORD:
680
+ temp.append(c)
681
+ c = self.format_next(i)
682
+ elif c in _DIGIT:
683
+ # Handle group number
684
+ temp.append(c)
685
+ c = self.format_next(i)
686
+ while c in _DIGIT:
687
+ temp.append(c)
688
+ c = self.format_next(i)
689
+
690
+ # Try and covert to integer index
691
+ field = ''.join(temp).strip()
692
+ try:
693
+ value = [(_util.FMT_FIELD, str(int(field, 10)))]
694
+ except ValueError:
695
+ value = [(_util.FMT_FIELD, field)]
696
+ pass
697
+
698
+ if c != '[':
699
+ value.append((_util.FMT_INDEX, None))
700
+
701
+ # Attributes and indexes
702
+ while c in ('[', '.'):
703
+ if c == '[':
704
+ findex = []
705
+ sindex = i.index - 1
706
+ c = self.format_next(i)
707
+ try:
708
+ while c != ']':
709
+ findex.append(c)
710
+ c = self.format_next(i)
711
+ except StopIteration as e:
712
+ raise PatternError(f"Unmatched '[' at {sindex - 1}") from e
713
+ idx = self.parse_format_index(''.join(findex))
714
+ value.append((_util.FMT_INDEX, idx))
715
+ c = self.format_next(i)
716
+ else:
717
+ findex = []
718
+ c = self.format_next(i)
719
+ while c in _WORD:
720
+ findex.append(c)
721
+ c = self.format_next(i)
722
+ value.append((_util.FMT_ATTR, ''.join(findex)))
723
+
724
+ # Conversion
725
+ if c == '!':
726
+ c = self.format_next(i)
727
+ if c not in _FMT_CONV_TYPE:
728
+ raise PatternError(f"Invalid conversion type at {i.index - 1}!")
729
+ value.append((_util.FMT_CONV, c))
730
+ c = self.format_next(i)
731
+
732
+ # Format spec
733
+ if c == ':':
734
+ fill = None # type: str | None
735
+ width = []
736
+ align = None
737
+ convert = None
738
+ c = self.format_next(i)
739
+
740
+ if c in ('<', '>', '^'):
741
+ # Get fill and alignment
742
+ align = c
743
+ c = self.format_next(i)
744
+ if c in ('<', '>', '^'):
745
+ fill = align
746
+ align = c
747
+ c = self.format_next(i)
748
+ elif c in _DIGIT:
749
+ # Get Width
750
+ fill = c
751
+ c = self.format_next(i)
752
+ if c in ('<', '>', '^'):
753
+ align = c
754
+ c = self.format_next(i)
755
+ else:
756
+ width.append(fill)
757
+ fill = None
758
+ else:
759
+ fill = c
760
+ c = self.format_next(i)
761
+ if fill == 's' and c == '}':
762
+ convert = fill
763
+ fill = None
764
+ if fill is not None:
765
+ if c not in ('<', '>', '^'):
766
+ raise PatternError(f'Invalid format spec char at {i.index - 1}!')
767
+ align = c
768
+ c = self.format_next(i)
769
+
770
+ while c in _DIGIT:
771
+ width.append(c)
772
+ c = self.format_next(i)
773
+
774
+ if not align and len(width) and width[0] == '0':
775
+ raise PatternError("'=' alignment is not supported!")
776
+ if align and not fill and len(width) and width[0] == '0':
777
+ fill = '0'
778
+
779
+ if c == 's':
780
+ convert = c
781
+ c = self.format_next(i)
782
+
783
+ if not fill:
784
+ fill = ' '
785
+
786
+ value.append(
787
+ (
788
+ _util.FMT_SPEC,
789
+ (
790
+ fill.encode('latin-1') if self.is_bytes else fill,
791
+ align,
792
+ (int(''.join(width)) if width else 0),
793
+ convert
794
+ )
795
+ )
796
+ )
797
+
798
+ if c != '}':
799
+ raise PatternError(f"Unmatched '{{' at {index - 1}")
800
+ except StopIteration as e:
801
+ raise PatternError(f"Unmatched '{{' at {index - 1}!") from e
802
+
803
+ return field, value
804
+
805
+ def handle_format(self, t: str, i: _util.StringIter) -> None:
806
+ """Handle format."""
807
+
808
+ if t == '{':
809
+ t = self.format_next(i)
810
+ if t == '{':
811
+ self.get_single_stack()
812
+ self.result.append(t)
813
+ else:
814
+ field, text = self.get_format(t, i)
815
+ self.handle_format_group(field, text)
816
+ else:
817
+ t = self.format_next(i)
818
+ if t == '}':
819
+ self.get_single_stack()
820
+ self.result.append(t)
821
+ else:
822
+ raise PatternError(f"Unmatched '}}' at {i.index - 2}!")
823
+
824
+ def get_octal(self, c: str, i: _util.StringIter) -> str | None:
825
+ """Get octal."""
826
+
827
+ index = i.index
828
+ value = []
829
+ zero_count = 0
830
+ try:
831
+ if c == '0':
832
+ for _ in range(3):
833
+ if c != '0':
834
+ break
835
+ value.append(c)
836
+ c = next(i)
837
+ zero_count = len(value)
838
+ if zero_count < 3:
839
+ for _ in range(3 - zero_count):
840
+ if c not in _OCTAL:
841
+ break
842
+ value.append(c)
843
+ c = next(i)
844
+ i.rewind(1)
845
+ except StopIteration:
846
+ pass
847
+
848
+ octal_count = len(value)
849
+ if not (self.use_format and octal_count) and not (zero_count and octal_count < 3) and octal_count != 3:
850
+ i.rewind(i.index - index)
851
+ value = []
852
+
853
+ return ''.join(value) if value else None
854
+
855
+ def parse_octal(self, text: str, i: _util.StringIter) -> None:
856
+ """Parse octal value."""
857
+
858
+ value = int(text, 8)
859
+ if value > 0xFF and self.is_bytes:
860
+ # Re fails on octal greater than `0o377` or `0xFF`
861
+ raise PatternError("Octal escape value outside of range 0-0o377!")
862
+ else:
863
+ single = self.get_single_stack()
864
+ if self.span_stack:
865
+ text = self.convert_case(chr(value), self.span_stack[-1])
866
+ value = ord(self.convert_case(text, single)) if single is not None else ord(text)
867
+ elif single:
868
+ value = ord(self.convert_case(chr(value), single))
869
+ if self.use_format and value in _CURLY_BRACKETS_ORD:
870
+ self.handle_format(chr(value), i)
871
+ elif value <= 0xFF:
872
+ self.result.append(f'\\{value:03o}')
873
+ else:
874
+ self.result.append(chr(value))
875
+
876
+ def get_named_unicode(self, i: _util.StringIter) -> str:
877
+ """Get named Unicode."""
878
+
879
+ index = i.index
880
+ value = []
881
+ try:
882
+ if next(i) != '{':
883
+ raise PatternError(f"Named Unicode missing '{{' at {i.index - 1}!")
884
+ c = next(i)
885
+ while c != '}':
886
+ value.append(c)
887
+ c = next(i)
888
+ except StopIteration as e:
889
+ raise PatternError(f"Unmatched '}}' at {index}!") from e
890
+
891
+ return ''.join(value)
892
+
893
+ def parse_named_unicode(self, i: _util.StringIter) -> None:
894
+ """Parse named Unicode."""
895
+
896
+ value = ord(_unicodedata.lookup(self.get_named_unicode(i)))
897
+ single = self.get_single_stack()
898
+ if self.span_stack:
899
+ text = self.convert_case(chr(value), self.span_stack[-1])
900
+ value = ord(self.convert_case(text, single)) if single is not None else ord(text)
901
+ elif single:
902
+ value = ord(self.convert_case(chr(value), single))
903
+ if self.use_format and value in _CURLY_BRACKETS_ORD:
904
+ self.handle_format(chr(value), i)
905
+ elif value <= 0xFF:
906
+ self.result.append(f'\\{value:03o}')
907
+ else:
908
+ self.result.append(chr(value))
909
+
910
+ def get_wide_unicode(self, i: _util.StringIter) -> str:
911
+ """Get narrow Unicode."""
912
+
913
+ value = []
914
+ for _ in range(3):
915
+ c = next(i)
916
+ if c == '0':
917
+ value.append(c)
918
+ else: # pragma: no cover
919
+ raise PatternError(f'Invalid wide Unicode character at {i.index - 1}!')
920
+
921
+ c = next(i)
922
+ if c in ('0', '1'):
923
+ value.append(c)
924
+ else: # pragma: no cover
925
+ raise PatternError(f'Invalid wide Unicode character at {i.index - 1}!')
926
+
927
+ for _ in range(4):
928
+ c = next(i)
929
+ if c.lower() in _HEX:
930
+ value.append(c)
931
+ else: # pragma: no cover
932
+ raise PatternError(f'Invalid wide Unicode character at {i.index - 1}!')
933
+ return ''.join(value)
934
+
935
+ def get_narrow_unicode(self, i: _util.StringIter) -> str:
936
+ """Get narrow Unicode."""
937
+
938
+ value = []
939
+ for _ in range(4):
940
+ c = next(i)
941
+ if c.lower() in _HEX:
942
+ value.append(c)
943
+ else: # pragma: no cover
944
+ raise PatternError(f'Invalid Unicode character at {i.index - 1}!')
945
+ return ''.join(value)
946
+
947
+ def parse_unicode(self, i: _util.StringIter, wide: bool = False) -> None:
948
+ """Parse Unicode."""
949
+
950
+ text = self.get_wide_unicode(i) if wide else self.get_narrow_unicode(i)
951
+ value = int(text, 16)
952
+ single = self.get_single_stack()
953
+ if self.span_stack:
954
+ text = self.convert_case(chr(value), self.span_stack[-1])
955
+ value = ord(self.convert_case(text, single)) if single is not None else ord(text)
956
+ elif single:
957
+ value = ord(self.convert_case(chr(value), single))
958
+ if self.use_format and value in _CURLY_BRACKETS_ORD:
959
+ self.handle_format(chr(value), i)
960
+ elif value <= 0xFF:
961
+ self.result.append(f'\\{value:03o}')
962
+ else:
963
+ self.result.append(chr(value))
964
+
965
+ def get_byte(self, i: _util.StringIter) -> str:
966
+ """Get byte."""
967
+
968
+ value = []
969
+ for _x in range(2):
970
+ c = next(i)
971
+ if c.lower() in _HEX:
972
+ value.append(c)
973
+ else: # pragma: no cover
974
+ raise PatternError(f'Invalid byte character at {i.index - 1}!')
975
+ return ''.join(value)
976
+
977
+ def parse_bytes(self, i: _util.StringIter) -> None:
978
+ """Parse byte."""
979
+
980
+ value = int(self.get_byte(i), 16)
981
+ single = self.get_single_stack()
982
+ if self.span_stack:
983
+ text = self.convert_case(chr(value), self.span_stack[-1])
984
+ value = ord(self.convert_case(text, single)) if single is not None else ord(text)
985
+ elif single:
986
+ value = ord(self.convert_case(chr(value), single))
987
+ if self.use_format and value in _CURLY_BRACKETS_ORD:
988
+ self.handle_format(chr(value), i)
989
+ else:
990
+ self.result.append(f'\\{value:03o}')
991
+
992
+ def get_named_group(self, t: str, i: _util.StringIter) -> str:
993
+ """Get group number."""
994
+
995
+ index = i.index
996
+ value = [t]
997
+ try:
998
+ c = next(i)
999
+ if c != "<":
1000
+ raise PatternError(f"Group missing '<' at {i.index - 1}!")
1001
+ value.append(c)
1002
+ c = next(i)
1003
+ if c in _DIGIT:
1004
+ value.append(c)
1005
+ c = next(i)
1006
+ while c != '>':
1007
+ if c in _DIGIT:
1008
+ value.append(c)
1009
+ c = next(i)
1010
+ value.append(c)
1011
+ elif c in _LETTERS_UNDERSCORE:
1012
+ value.append(c)
1013
+ c = next(i)
1014
+ while c != '>':
1015
+ if c in _WORD:
1016
+ value.append(c)
1017
+ c = next(i)
1018
+ value.append(c)
1019
+ else:
1020
+ raise PatternError(f"Invalid group character at {i.index - 1}!")
1021
+ except StopIteration as e:
1022
+ raise PatternError(f"Unmatched '<' at {index}!") from e
1023
+
1024
+ return ''.join(value)
1025
+
1026
+ def get_group(self, t: str, i: _util.StringIter) -> str | None:
1027
+ """Get group number."""
1028
+
1029
+ value = []
1030
+ try:
1031
+ if t in _DIGIT and t != '0':
1032
+ value.append(t)
1033
+ t = next(i)
1034
+ if t in _DIGIT:
1035
+ value.append(t)
1036
+ else:
1037
+ i.rewind(1)
1038
+ except StopIteration:
1039
+ pass
1040
+ return ''.join(value) if value else None
1041
+
1042
+ def format_next(self, i: _util.StringIter) -> str:
1043
+ """Get next format char."""
1044
+
1045
+ c = next(i)
1046
+ return self.format_references(next(i), i) if c == '\\' else c
1047
+
1048
+ def format_references(self, t: str, i: _util.StringIter) -> str:
1049
+ """Handle format references."""
1050
+
1051
+ octal = self.get_octal(t, i)
1052
+ if octal:
1053
+ o = int(octal, 8)
1054
+ if o > 0xFF and self.is_bytes:
1055
+ # Re fails on octal greater than `0o377` or `0xFF`
1056
+ raise PatternError("Octal escape value outside of range 0-0o377!")
1057
+ value = chr(o)
1058
+ elif t in _STANDARD_ESCAPES or t == '\\':
1059
+ value = _BACK_SLASH_TRANSLATION['\\' + t]
1060
+ elif not self.is_bytes and t == "U":
1061
+ value = chr(int(self.get_wide_unicode(i), 16))
1062
+ elif not self.is_bytes and t == "u":
1063
+ value = chr(int(self.get_narrow_unicode(i), 16))
1064
+ elif not self.is_bytes and t == "N":
1065
+ value = _unicodedata.lookup(self.get_named_unicode(i))
1066
+ elif t == "x":
1067
+ value = chr(int(self.get_byte(i), 16))
1068
+ else:
1069
+ i.rewind(1)
1070
+ value = '\\'
1071
+ return value
1072
+
1073
+ def reference(self, t: str, i: _util.StringIter) -> None:
1074
+ """Handle references."""
1075
+ octal = self.get_octal(t, i)
1076
+ if t in _OCTAL and octal:
1077
+ self.parse_octal(octal, i)
1078
+ elif (t in _DIGIT or t == 'g') and not self.use_format:
1079
+ group = self.get_group(t, i)
1080
+ if not group:
1081
+ group = self.get_named_group(t, i)
1082
+ self.handle_group('\\' + group)
1083
+ elif t in _STANDARD_ESCAPES:
1084
+ self.get_single_stack()
1085
+ self.result.append('\\' + t)
1086
+ elif t == "l":
1087
+ self.single_case(i, _LOWER)
1088
+ elif t == "L":
1089
+ self.span_case(i, _LOWER)
1090
+ elif t == "c":
1091
+ self.single_case(i, _UPPER)
1092
+ elif t == "C":
1093
+ self.span_case(i, _UPPER)
1094
+ elif t == "E":
1095
+ self.end_found = True
1096
+ elif not self.is_bytes and t == "U":
1097
+ self.parse_unicode(i, True)
1098
+ elif not self.is_bytes and t == "u":
1099
+ self.parse_unicode(i)
1100
+ elif not self.is_bytes and t == "N":
1101
+ self.parse_named_unicode(i)
1102
+ elif t == "x":
1103
+ self.parse_bytes(i)
1104
+ elif self.use_format and t in _CURLY_BRACKETS:
1105
+ self.result.append('\\\\')
1106
+ self.handle_format(t, i)
1107
+ elif self.use_format and t == 'g':
1108
+ self.result.append('\\\\')
1109
+ self.result.append(t)
1110
+ else:
1111
+ value = '\\' + t
1112
+ self.get_single_stack()
1113
+ if self.span_stack:
1114
+ value = self.convert_case(value, self.span_stack[-1])
1115
+ self.result.append(value)
1116
+
1117
+ def _parse_template(self, template: str) -> str:
1118
+ """Parse template."""
1119
+
1120
+ self.result = [""]
1121
+
1122
+ i = _util.StringIter(template)
1123
+
1124
+ try:
1125
+ while True:
1126
+ t = next(i)
1127
+ if self.use_format and t in _CURLY_BRACKETS:
1128
+ self.handle_format(t, i)
1129
+ elif t == '\\':
1130
+ try:
1131
+ t = next(i)
1132
+ self.reference(t, i)
1133
+ except StopIteration:
1134
+ self.result.append(t)
1135
+ raise
1136
+ else:
1137
+ self.result.append(t)
1138
+ except StopIteration:
1139
+ pass
1140
+
1141
+ if len(self.result) > 1:
1142
+ self.literal_slots.append("".join(self.result))
1143
+ del self.result[:]
1144
+ self.result.append("")
1145
+ self.slot += 1
1146
+
1147
+ return "".join(self.literal_slots)
1148
+
1149
+ def parse_template(self) -> None:
1150
+ """Parse template."""
1151
+
1152
+ if isinstance(self._original, bytes):
1153
+ self._template = self._parse_template(self._original.decode('latin-1')).encode('latin-1')
1154
+ else:
1155
+ self._template = self._parse_template(self._original)
1156
+
1157
+ if _util.PY312:
1158
+ count = 0
1159
+ for part in _parser.parse_template(self._template, self.pattern):
1160
+ if isinstance(part, int):
1161
+ self.groups.append((count, part))
1162
+ self.literals.append(None)
1163
+ elif part:
1164
+ self.literals.append(cast(AnyStr, part))
1165
+ else:
1166
+ continue
1167
+ count += 1
1168
+ else:
1169
+ self.groups, self.literals = _parser.parse_template(self._template, self.pattern)
1170
+
1171
+ def span_case(self, i: _util.StringIter, case: int) -> None:
1172
+ """Uppercase or lowercase the next range of characters until end marker is found."""
1173
+
1174
+ # A new \L, \C or \E should pop the last in the stack.
1175
+ if self.span_stack:
1176
+ self.span_stack.pop()
1177
+ if self.single_stack:
1178
+ self.single_stack.pop()
1179
+ self.span_stack.append(case)
1180
+ count = len(self.span_stack)
1181
+ self.end_found = False
1182
+ try:
1183
+ while not self.end_found:
1184
+ t = next(i)
1185
+ if self.use_format and t in _CURLY_BRACKETS:
1186
+ self.handle_format(t, i)
1187
+ elif t == '\\':
1188
+ try:
1189
+ t = next(i)
1190
+ self.reference(t, i)
1191
+ except StopIteration:
1192
+ self.result.append(t)
1193
+ raise
1194
+ else:
1195
+ self.result.append(self.convert_case(t, case))
1196
+ if self.end_found or count > len(self.span_stack):
1197
+ self.end_found = False
1198
+ break
1199
+ except StopIteration:
1200
+ pass
1201
+ if count == len(self.span_stack):
1202
+ self.span_stack.pop()
1203
+
1204
+ def convert_case(self, value: str, case: int) -> str:
1205
+ """Convert case."""
1206
+
1207
+ if self.is_bytes:
1208
+ cased = []
1209
+ for c in value:
1210
+ if c in _ASCII_LETTERS:
1211
+ cased.append(c.lower() if case == _LOWER else c.upper())
1212
+ else:
1213
+ cased.append(c)
1214
+ return "".join(cased)
1215
+ else:
1216
+ return value.lower() if case == _LOWER else value.upper()
1217
+
1218
+ def single_case(self, i: _util.StringIter, case: int) -> None:
1219
+ """Uppercase or lowercase the next character."""
1220
+
1221
+ # Pop a previous case if we have consecutive ones.
1222
+ if self.single_stack:
1223
+ self.single_stack.pop()
1224
+ self.single_stack.append(case)
1225
+ try:
1226
+ t = next(i)
1227
+ if self.use_format and t in _CURLY_BRACKETS:
1228
+ self.handle_format(t, i)
1229
+ elif t == '\\':
1230
+ try:
1231
+ t = next(i)
1232
+ self.reference(t, i)
1233
+ except StopIteration:
1234
+ self.result.append(t)
1235
+ raise
1236
+ elif self.single_stack:
1237
+ this_case = self.get_single_stack()
1238
+ if this_case is not None:
1239
+ self.result.append(self.convert_case(t, this_case))
1240
+ except StopIteration:
1241
+ pass
1242
+
1243
+ def get_single_stack(self) -> int | None:
1244
+ """Get the correct single stack item to use."""
1245
+
1246
+ single = None
1247
+ while self.single_stack:
1248
+ single = self.single_stack.pop()
1249
+ return single
1250
+
1251
+ def handle_format_group(self, field: str, text: list[tuple[int, Any]]) -> None:
1252
+ """Handle format group."""
1253
+
1254
+ # Handle auto incrementing group indexes
1255
+ if field == '':
1256
+ if self.auto:
1257
+ field = str(self.auto_index)
1258
+ text[0] = (_util.FMT_FIELD, field)
1259
+ self.auto_index += 1
1260
+ elif not self.manual and not self.auto:
1261
+ self.auto = True
1262
+ field = str(self.auto_index)
1263
+ text[0] = (_util.FMT_FIELD, field)
1264
+ self.auto_index += 1
1265
+ else:
1266
+ raise PatternError("Cannot switch to auto format during manual format!")
1267
+ elif not self.manual and not self.auto:
1268
+ self.manual = True
1269
+ elif not self.manual:
1270
+ raise PatternError("Cannot switch to manual format during auto format!")
1271
+
1272
+ self.handle_group(field, tuple(text), True)
1273
+
1274
+ def handle_group(
1275
+ self,
1276
+ text: str,
1277
+ capture: tuple[tuple[int, Any], ...] | None = None,
1278
+ is_format: bool = False
1279
+ ) -> None:
1280
+ """Handle groups."""
1281
+
1282
+ if len(self.result) > 1:
1283
+ self.literal_slots.append("".join(self.result))
1284
+ if is_format:
1285
+ self.literal_slots.extend(["\\g<", text, ">"])
1286
+ else:
1287
+ self.literal_slots.append(text)
1288
+ del self.result[:]
1289
+ self.result.append("")
1290
+ self.slot += 1
1291
+ elif is_format:
1292
+ self.literal_slots.extend(["\\g<", text, ">"])
1293
+ else:
1294
+ self.literal_slots.append(text)
1295
+
1296
+ self.group_slots.append(
1297
+ (
1298
+ self.slot,
1299
+ (
1300
+ (self.span_stack[-1] if self.span_stack else None),
1301
+ self.get_single_stack(),
1302
+ (() if self.is_bytes else '') if capture is None else capture
1303
+ )
1304
+ )
1305
+ )
1306
+ self.slot += 1
1307
+
1308
+ def get_base_template(self) -> AnyStr:
1309
+ """Return the unmodified template before expansion."""
1310
+
1311
+ return self._original
1312
+
1313
+ def parse(self) -> ReplaceTemplate[AnyStr]:
1314
+ """Parse template."""
1315
+
1316
+ if not isinstance(self.pattern.pattern, type(self._original)):
1317
+ raise TypeError('Pattern string type must match replace template string type!')
1318
+
1319
+ self.parse_template()
1320
+
1321
+ return ReplaceTemplate(
1322
+ tuple(self.groups),
1323
+ tuple(self.group_slots),
1324
+ tuple(self.literals),
1325
+ hash(self.pattern),
1326
+ self.use_format,
1327
+ self.is_bytes
1328
+ )
1329
+
1330
+
1331
+ class ReplaceTemplate(_util.Immutable, Generic[AnyStr]):
1332
+ """Replacement template expander."""
1333
+
1334
+ __slots__ = ("groups", "group_slots", "literals", "pattern_hash", "use_format", "_hash", "_bytes")
1335
+
1336
+ groups: tuple[tuple[int, int], ...]
1337
+ group_slots: tuple[tuple[int, tuple[int | None, int | None, Any]], ...]
1338
+ literals: tuple[AnyStr | None, ...]
1339
+ pattern_hash: int
1340
+ use_format: bool
1341
+ _hash: int
1342
+ _bytes: bool
1343
+
1344
+ def __init__(
1345
+ self,
1346
+ groups: tuple[tuple[int, int], ...],
1347
+ group_slots: tuple[tuple[int, tuple[int | None, int | None, Any]], ...],
1348
+ literals: tuple[AnyStr | None, ...],
1349
+ pattern_hash: int,
1350
+ use_format: bool,
1351
+ is_bytes: bool
1352
+ ) -> None:
1353
+ """Initialize."""
1354
+
1355
+ super().__init__(
1356
+ use_format=use_format,
1357
+ groups=groups,
1358
+ group_slots=group_slots,
1359
+ literals=literals,
1360
+ pattern_hash=pattern_hash,
1361
+ _bytes=is_bytes,
1362
+ _hash=hash(
1363
+ (
1364
+ type(self),
1365
+ groups, group_slots, literals,
1366
+ pattern_hash, use_format, is_bytes
1367
+ )
1368
+ )
1369
+ )
1370
+
1371
+ def __call__(self, m: Match[AnyStr] | None) -> AnyStr:
1372
+ """Call."""
1373
+
1374
+ return self.expand(m)
1375
+
1376
+ def __hash__(self) -> int:
1377
+ """Hash."""
1378
+
1379
+ return self._hash
1380
+
1381
+ def __eq__(self, other: Any) -> bool:
1382
+ """Equal."""
1383
+
1384
+ return (
1385
+ isinstance(other, ReplaceTemplate) and
1386
+ self.groups == other.groups and
1387
+ self.group_slots == other.group_slots and
1388
+ self.literals == other.literals and
1389
+ self.pattern_hash == other.pattern_hash and
1390
+ self.use_format == other.use_format and
1391
+ self._bytes == other._bytes
1392
+ )
1393
+
1394
+ def __ne__(self, other: Any) -> bool:
1395
+ """Equal."""
1396
+
1397
+ return (
1398
+ not isinstance(other, ReplaceTemplate) or
1399
+ self.groups != other.groups or
1400
+ self.group_slots != other.group_slots or
1401
+ self.literals != other.literals or
1402
+ self.pattern_hash != other.pattern_hash or
1403
+ self.use_format != other.use_format or
1404
+ self._bytes != self._bytes
1405
+ )
1406
+
1407
+ def __repr__(self) -> str: # pragma: no cover
1408
+ """Representation."""
1409
+
1410
+ return "{}.{}({!r}, {!r}, {!r}, {!r}, {!r})".format(
1411
+ self.__module__, self.__class__.__name__,
1412
+ self.groups, self.group_slots, self.literals,
1413
+ self.pattern_hash, self.use_format
1414
+ )
1415
+
1416
+ def _get_group_index(self, index: int) -> int:
1417
+ """Find and return the appropriate group index."""
1418
+
1419
+ g_index = 0
1420
+ for group in self.groups:
1421
+ if group[0] == index:
1422
+ g_index = group[1]
1423
+ break
1424
+ return g_index
1425
+
1426
+ def _get_group_attributes(self, index: int) -> tuple[int | None, int | None, Any]:
1427
+ """Find and return the appropriate group case."""
1428
+
1429
+ g_case = (None, None, -1) # type: tuple[int | None, int | None, Any]
1430
+ for group in self.group_slots:
1431
+ if group[0] == index:
1432
+ g_case = group[1]
1433
+ break
1434
+ return g_case
1435
+
1436
+ def expand(self, m: Match[AnyStr] | None) -> AnyStr:
1437
+ """Using the template, expand the string."""
1438
+
1439
+ if m is None:
1440
+ raise ValueError("Match is None!")
1441
+
1442
+ sep = m.string[:0]
1443
+ if not isinstance(sep, bytes if self._bytes else str):
1444
+ raise TypeError('Match string type does not match expander string type!')
1445
+ text = []
1446
+ # Expand string
1447
+ for x in range(0, len(self.literals)):
1448
+ index = x
1449
+ l = self.literals[x]
1450
+ if l is None:
1451
+ g_index = self._get_group_index(index)
1452
+ span_case, single_case, capture = self._get_group_attributes(index)
1453
+ if not self.use_format:
1454
+ # Non format replace
1455
+ try:
1456
+ l = m.group(g_index)
1457
+ if l is None:
1458
+ l = sep
1459
+ except IndexError as e: # pragma: no cover
1460
+ raise IndexError(f"'{g_index}' is out of range!") from e
1461
+ else:
1462
+ # String format replace
1463
+ try:
1464
+ obj = m.group(g_index)
1465
+ except IndexError as e: # pragma: no cover
1466
+ raise IndexError(f"'{g_index}' is out of range!") from e
1467
+ l = _util.format_captures(
1468
+ [] if obj is None else [obj],
1469
+ capture,
1470
+ _util._to_bstr if isinstance(sep, bytes) else _util._to_str,
1471
+ sep
1472
+ )
1473
+ if span_case is not None:
1474
+ if span_case == _LOWER:
1475
+ l = l.lower()
1476
+ else:
1477
+ l = l.upper()
1478
+ if single_case is not None:
1479
+ if single_case == _LOWER:
1480
+ l = l[0:1].lower() + l[1:]
1481
+ else:
1482
+ l = l[0:1].upper() + l[1:]
1483
+ text.append(l)
1484
+
1485
+ return sep.join(text)
1486
+
1487
+
1488
+ def _pickle(r): # type: ignore[no-untyped-def]
1489
+ """Pickle."""
1490
+
1491
+ return ReplaceTemplate, (r.groups, r.group_slots, r.literals, r.pattern_hash, r.use_format, r._bytes)
1492
+
1493
+
1494
+ _copyreg.pickle(ReplaceTemplate, _pickle)