backrefs 8.2__tar.gz → 8.3__tar.gz
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.
- {backrefs-8.2 → backrefs-8.3}/PKG-INFO +1 -1
- {backrefs-8.2 → backrefs-8.3}/backrefs/__meta__.py +1 -1
- {backrefs-8.2 → backrefs-8.3}/backrefs/_bre_parse.py +179 -28
- {backrefs-8.2 → backrefs-8.3}/backrefs/_bregex_parse.py +176 -91
- {backrefs-8.2 → backrefs-8.3}/backrefs/bre.py +3 -2
- {backrefs-8.2 → backrefs-8.3}/backrefs/bregex.py +3 -2
- {backrefs-8.2 → backrefs-8.3}/backrefs/uniprops/__init__.py +99 -62
- {backrefs-8.2 → backrefs-8.3}/docs/src/dictionary/en-custom.txt +3 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/markdown/about/changelog.md +34 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/markdown/refs.md +16 -3
- {backrefs-8.2 → backrefs-8.3}/tests/test_binary.py +41 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_block.py +53 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_bre.py +248 -8
- {backrefs-8.2 → backrefs-8.3}/tests/test_bregex.py +108 -4
- {backrefs-8.2 → backrefs-8.3}/tests/test_generalcategory.py +45 -0
- {backrefs-8.2 → backrefs-8.3}/tools/unipropgen.py +11 -0
- backrefs-8.2/tools/unicodedata/13.0.0.zip +0 -0
- {backrefs-8.2 → backrefs-8.3}/.coveragerc +0 -0
- {backrefs-8.2 → backrefs-8.3}/.gitignore +0 -0
- {backrefs-8.2 → backrefs-8.3}/.pyspelling.yml +0 -0
- {backrefs-8.2 → backrefs-8.3}/LICENSE.md +0 -0
- {backrefs-8.2 → backrefs-8.3}/README.md +0 -0
- {backrefs-8.2 → backrefs-8.3}/backrefs/__init__.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/backrefs/_bregex_typing.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/backrefs/py.typed +0 -0
- {backrefs-8.2 → backrefs-8.3}/backrefs/util.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/dictionary/en-python.txt +0 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/markdown/.snippets/links.md +0 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/markdown/about/contributing.md +0 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/markdown/about/license.md +0 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/markdown/about/security.md +0 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/markdown/index.md +0 -0
- {backrefs-8.2 → backrefs-8.3}/docs/src/markdown/usage.md +0 -0
- {backrefs-8.2 → backrefs-8.3}/docs/theme/announce.html +0 -0
- {backrefs-8.2 → backrefs-8.3}/hatch_build.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/pyproject.toml +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/__init__.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_age.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_bidiclass.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_bidipairedbrackettype.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_canonicalcombiningclass.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_decompositiontype.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_eastasianwidth.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_graphemeclusterbreak.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_hangulsyllabletype.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_indicpositionalcategory.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_indicsylabiccategory.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_joininggroup.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_joiningtype.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_linebreak.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_numerictype.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_numericvalue.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_quickcheck.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_script.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_scriptextensions.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_sentencebreak.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_uniprops.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_versions.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_verticalorientation.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tests/test_wordbreak.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/__init__.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/pycon.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/unicodedata/14.0.0.zip +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/unicodedata/15.0.0.zip +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/unicodedata/15.1.0.zip +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/unicodedata/16.0.0.zip +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/unicodedata/17.0.0.zip +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/unicodedata/LICENSE +0 -0
- {backrefs-8.2 → backrefs-8.3}/tools/unidatadownload.py +0 -0
- {backrefs-8.2 → backrefs-8.3}/zensical.yml +0 -0
|
@@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
|
|
|
193
193
|
return Version(major, minor, micro, release, pre, post, dev)
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
__version_info__ = Version(8,
|
|
196
|
+
__version_info__ = Version(8, 3, 0, "final")
|
|
197
197
|
__version__ = __version_info__._get_canonical()
|
|
@@ -6,18 +6,13 @@ Copyright (c) 2011 - 2020 Isaac Muse <isaacmuse@gmail.com>
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
import re as _re
|
|
9
|
-
import
|
|
9
|
+
import re._parser as _parser # type: ignore[import]
|
|
10
10
|
import copyreg as _copyreg
|
|
11
11
|
from . import util as _util
|
|
12
12
|
from .util import PatternError
|
|
13
13
|
from . import uniprops as _uniprops
|
|
14
14
|
from typing import Generic, AnyStr, Match, Any, Pattern, cast
|
|
15
15
|
|
|
16
|
-
if sys.version_info >= (3, 11):
|
|
17
|
-
import re._parser as _parser # type: ignore[import]
|
|
18
|
-
else:
|
|
19
|
-
import sre_parse as _parser
|
|
20
|
-
|
|
21
16
|
__all__ = ("ReplaceTemplate", "PatternError")
|
|
22
17
|
|
|
23
18
|
_ASCII_LETTERS = frozenset(
|
|
@@ -76,11 +71,13 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
76
71
|
_re_start_wb = r"\b(?=\w)"
|
|
77
72
|
_re_end_wb = r"\b(?<=\w)"
|
|
78
73
|
_line_break = r'(?:\r\n|(?!\r\n)[\n\v\f\r\x85\u2028\u2029])'
|
|
79
|
-
_bytes_line_break = r'(?>\r\n|[\n\v\f\r\x85])'
|
|
74
|
+
_bytes_line_break = r'(?>\r\n|[\n\v\f\r\x85])'
|
|
80
75
|
_grapheme_cluster = r'(?:{}{}*(?!{}))'
|
|
81
76
|
|
|
82
77
|
verbose: bool
|
|
83
78
|
unicode: bool
|
|
79
|
+
named_unicode: tuple[str, int]
|
|
80
|
+
negated_members: list[str]
|
|
84
81
|
ascii: bool # noqa: A003
|
|
85
82
|
is_bytes: bool
|
|
86
83
|
search: AnyStr
|
|
@@ -283,6 +280,7 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
283
280
|
current.extend(self.unicode_name(text, in_group))
|
|
284
281
|
if in_group:
|
|
285
282
|
self.found_named_unicode = True
|
|
283
|
+
self.named_unicode = (f'\\N{{{text}}}', ord(_util.unicode_char(text)))
|
|
286
284
|
else:
|
|
287
285
|
current.extend(["\\", t])
|
|
288
286
|
return current
|
|
@@ -418,27 +416,57 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
418
416
|
self.verbose = verbose
|
|
419
417
|
self.unicode = unicode_flag
|
|
420
418
|
|
|
421
|
-
|
|
422
|
-
current.append(t)
|
|
419
|
+
current.append(t)
|
|
423
420
|
return current
|
|
424
421
|
|
|
425
422
|
def char_groups(self, t: str, i: _util.StringIter) -> list[str]:
|
|
426
423
|
"""Handle character groups."""
|
|
427
424
|
|
|
428
|
-
current = []
|
|
425
|
+
current = [] # type: list[str]
|
|
429
426
|
pos = i.index - 1
|
|
430
427
|
found = False
|
|
431
428
|
escaped = False
|
|
429
|
+
escape_index = 0
|
|
432
430
|
first = 0
|
|
433
431
|
found_property = False
|
|
432
|
+
# Index of an unescaped `-` in `current` that is acting as a range operator.
|
|
433
|
+
range_dash = -1
|
|
434
|
+
# Whether the last member could start a range if followed by `-`.
|
|
435
|
+
range_start = False
|
|
436
|
+
# The last member's original text, position, and code point if it is a named character
|
|
437
|
+
# beyond the byte range of a byte string.
|
|
438
|
+
start = ('', 0, None) # type: tuple[str, int, int | None]
|
|
434
439
|
self.found_property = False
|
|
435
440
|
self.found_named_unicode = False
|
|
441
|
+
self.negated_members = []
|
|
442
|
+
negated_set = False
|
|
443
|
+
|
|
444
|
+
def add_member(raw: str, index: int, value: int | None) -> None:
|
|
445
|
+
"""Track a member that was just added, which may start or end a range."""
|
|
446
|
+
|
|
447
|
+
nonlocal range_dash, range_start, start
|
|
448
|
+
|
|
449
|
+
if range_dash != -1:
|
|
450
|
+
# This member ends a range.
|
|
451
|
+
start_raw, start_index, start_value = start
|
|
452
|
+
if start_value is not None or value is not None:
|
|
453
|
+
end = self.get_named_byte_range(start_raw, raw, start_value, value, start_index)
|
|
454
|
+
if end is None:
|
|
455
|
+
current[range_dash] = ''
|
|
456
|
+
else:
|
|
457
|
+
current[-1] = end
|
|
458
|
+
range_dash = -1
|
|
459
|
+
range_start = False
|
|
460
|
+
else:
|
|
461
|
+
start = (raw, index, value)
|
|
462
|
+
range_start = True
|
|
436
463
|
|
|
437
464
|
try:
|
|
438
465
|
while True:
|
|
439
466
|
# Prevent POSIX/Unicode class from being part of a range.
|
|
440
467
|
if self.found_property and t == '-':
|
|
441
468
|
current.append(_re.escape(t))
|
|
469
|
+
add_member(t, i.index - 1, None)
|
|
442
470
|
pos += 1
|
|
443
471
|
t = next(i)
|
|
444
472
|
self.found_property = False
|
|
@@ -448,15 +476,26 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
448
476
|
|
|
449
477
|
if not escaped and t == "\\":
|
|
450
478
|
escaped = True
|
|
479
|
+
escape_index = i.index - 1
|
|
451
480
|
elif escaped:
|
|
452
481
|
escaped = False
|
|
453
482
|
idx = len(current) - 1
|
|
454
483
|
current.extend(self.reference(t, i, True))
|
|
455
484
|
if self.found_property:
|
|
456
485
|
# Prevent Unicode class from being part of a range.
|
|
457
|
-
if idx
|
|
486
|
+
if idx == range_dash:
|
|
458
487
|
current[idx] = _re.escape('-')
|
|
459
488
|
found_property = True
|
|
489
|
+
range_dash = -1
|
|
490
|
+
range_start = False
|
|
491
|
+
elif t == 'N':
|
|
492
|
+
raw, code = self.named_unicode
|
|
493
|
+
add_member(raw, escape_index, code if self.is_bytes and code > 0xFF else None)
|
|
494
|
+
else:
|
|
495
|
+
digits = self.get_escape_digits(i, t)
|
|
496
|
+
if digits:
|
|
497
|
+
current.append(digits)
|
|
498
|
+
add_member(f'\\{t}{digits}', escape_index, None)
|
|
460
499
|
elif t == "[" and not found:
|
|
461
500
|
found = True
|
|
462
501
|
first = pos
|
|
@@ -470,22 +509,44 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
470
509
|
i.rewind(i.index - index)
|
|
471
510
|
if prop is not None:
|
|
472
511
|
value = self.unicode_props(prop[0], prop[1], in_group=True)
|
|
473
|
-
|
|
512
|
+
# Prevent POSIX class from being part of a range.
|
|
513
|
+
if len(current) - 1 == range_dash:
|
|
474
514
|
current[-1] = _re.escape('-')
|
|
475
515
|
current.extend(value)
|
|
476
516
|
found_property = True
|
|
517
|
+
self.found_property = True
|
|
477
518
|
pos = i.index - 2
|
|
519
|
+
range_dash = -1
|
|
520
|
+
range_start = False
|
|
478
521
|
else:
|
|
479
522
|
current.append(t)
|
|
480
|
-
|
|
523
|
+
add_member(t, index - 1, None)
|
|
524
|
+
elif t == "^" and found and (pos == first + 1) and len(current) == 1:
|
|
525
|
+
# Only a `^` directly after the opening `[` negates the set.
|
|
481
526
|
first = pos
|
|
482
527
|
current.append(t)
|
|
528
|
+
negated_set = True
|
|
483
529
|
elif t == "]" and found and (pos != first + 1):
|
|
484
530
|
found = False
|
|
485
531
|
current.append(t)
|
|
486
532
|
break
|
|
487
|
-
|
|
533
|
+
elif t == '-' and range_start:
|
|
534
|
+
# A `-` after a member starts a range, unless it is followed by `]`.
|
|
488
535
|
current.append(t)
|
|
536
|
+
range_dash = len(current) - 1
|
|
537
|
+
range_start = False
|
|
538
|
+
else:
|
|
539
|
+
if (
|
|
540
|
+
t == '^' and
|
|
541
|
+
(found_property or self.found_named_unicode) and
|
|
542
|
+
''.join(current) in ('[', '[^')
|
|
543
|
+
):
|
|
544
|
+
# Everything before this was a property that resolved to nothing,
|
|
545
|
+
# so escape `^` to prevent it from being treated as a negation.
|
|
546
|
+
current.append(_re.escape(t))
|
|
547
|
+
else:
|
|
548
|
+
current.append(t)
|
|
549
|
+
add_member(t, i.index - 1, None)
|
|
489
550
|
pos += 1
|
|
490
551
|
t = next(i)
|
|
491
552
|
except StopIteration as e:
|
|
@@ -495,7 +556,9 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
495
556
|
# This will occur when a property's values exceed
|
|
496
557
|
# either the Unicode char limit on a narrow system,
|
|
497
558
|
# or the ASCII limit in a byte string pattern.
|
|
498
|
-
if
|
|
559
|
+
if self.negated_members:
|
|
560
|
+
current = [self.negated_set_text(''.join(current), negated_set)]
|
|
561
|
+
elif found_property or self.found_named_unicode:
|
|
499
562
|
temp = "".join(current)
|
|
500
563
|
if temp == '[]':
|
|
501
564
|
# We specified some properties, but they are all
|
|
@@ -508,6 +571,32 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
508
571
|
|
|
509
572
|
return current
|
|
510
573
|
|
|
574
|
+
def negated_set_text(self, text: str, negated: bool) -> str:
|
|
575
|
+
r"""
|
|
576
|
+
Get a character set that contains negated properties, such as `[a\P{Lu}]`.
|
|
577
|
+
|
|
578
|
+
Under case-insensitive matching, a negated property must be a negated set of the property's characters
|
|
579
|
+
(`[^...]`), as a set of the characters outside the property would also match the other case of the
|
|
580
|
+
characters in it. A set can't contain a negated set, so each part of the set is matched separately:
|
|
581
|
+
`[R\P{A}\P{B}]` becomes `(?:[R]|[^A]|[^B])`, and `[^R\P{A}\P{B}]` becomes `(?![R])(?=[A])[B]`.
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
members = text[2 if negated else 1:-1]
|
|
585
|
+
if members.startswith('^'):
|
|
586
|
+
# A literal `^` after the negating `^` would negate the members' own set.
|
|
587
|
+
members = '\\' + members
|
|
588
|
+
|
|
589
|
+
if not negated:
|
|
590
|
+
alternatives = [f'[^{m}]' for m in self.negated_members]
|
|
591
|
+
if members:
|
|
592
|
+
alternatives.insert(0, f'[{members}]')
|
|
593
|
+
return alternatives[0] if len(alternatives) == 1 else f"(?:{'|'.join(alternatives)})"
|
|
594
|
+
|
|
595
|
+
conditions = [f'(?=[{m}])' for m in self.negated_members[:-1]]
|
|
596
|
+
if members:
|
|
597
|
+
conditions.insert(0, f'(?![{members}])')
|
|
598
|
+
return ''.join(conditions) + f'[{self.negated_members[-1]}]'
|
|
599
|
+
|
|
511
600
|
def normal(self, t: str, i: _util.StringIter) -> list[str]:
|
|
512
601
|
"""Handle normal chars."""
|
|
513
602
|
|
|
@@ -540,6 +629,60 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
540
629
|
return ['']
|
|
541
630
|
return [f'\\{value:03o}' if value <= 0xFF else chr(value)]
|
|
542
631
|
|
|
632
|
+
def get_escape_digits(self, i: _util.StringIter, e: str) -> str:
|
|
633
|
+
r"""
|
|
634
|
+
Get the digits that are part of an escape in a character set (`\x`, `\u`, `\U`, or octal).
|
|
635
|
+
|
|
636
|
+
This allows an escape to be treated as a single member of the character set.
|
|
637
|
+
"""
|
|
638
|
+
|
|
639
|
+
if e == 'x':
|
|
640
|
+
count, valid = 2, _HEX
|
|
641
|
+
elif e == 'u' and not self.is_bytes:
|
|
642
|
+
count, valid = 4, _HEX
|
|
643
|
+
elif e == 'U' and not self.is_bytes:
|
|
644
|
+
count, valid = 8, _HEX
|
|
645
|
+
elif e in _OCTAL:
|
|
646
|
+
count, valid = 2, _OCTAL
|
|
647
|
+
else:
|
|
648
|
+
return ''
|
|
649
|
+
|
|
650
|
+
digits = []
|
|
651
|
+
while count:
|
|
652
|
+
try:
|
|
653
|
+
c = next(i)
|
|
654
|
+
except StopIteration:
|
|
655
|
+
break
|
|
656
|
+
if c.lower() not in valid:
|
|
657
|
+
i.rewind(1)
|
|
658
|
+
break
|
|
659
|
+
digits.append(c)
|
|
660
|
+
count -= 1
|
|
661
|
+
return ''.join(digits)
|
|
662
|
+
|
|
663
|
+
@staticmethod
|
|
664
|
+
def get_named_byte_range(
|
|
665
|
+
start: str,
|
|
666
|
+
end: str,
|
|
667
|
+
start_value: int | None,
|
|
668
|
+
end_value: int | None,
|
|
669
|
+
index: int
|
|
670
|
+
) -> str | None:
|
|
671
|
+
"""
|
|
672
|
+
Get the end of a range in a byte string where either end is a named character beyond the byte range.
|
|
673
|
+
|
|
674
|
+
`start_value` and `end_value` are the code points of the ends that are named characters beyond the byte
|
|
675
|
+
range, and `None` for ends within the byte range. `start` and `end` are the original text of each end,
|
|
676
|
+
and `index` is where the range starts, both used for errors.
|
|
677
|
+
|
|
678
|
+
A range that ends beyond the byte range includes everything up to the end of the byte range, so return
|
|
679
|
+
the end to use in its place. A range that starts beyond the byte range matches nothing, so return `None`.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
if start_value is not None and (end_value is None or end_value < start_value):
|
|
683
|
+
raise PatternError(f'bad character range {start}-{end} at position {index}')
|
|
684
|
+
return None if start_value is not None else '\\377'
|
|
685
|
+
|
|
543
686
|
def unicode_props(
|
|
544
687
|
self,
|
|
545
688
|
props: str,
|
|
@@ -569,14 +712,23 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
569
712
|
else:
|
|
570
713
|
mode = _uniprops.MODE_UNICODE
|
|
571
714
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
715
|
+
# Negated properties are expressed as a negated set of the property's characters, so that they work with
|
|
716
|
+
# case-insensitive matching (see `negated_set_text`).
|
|
717
|
+
v, negated = _uniprops.get_unicode_property_parts(props, prop_value, mode)
|
|
718
|
+
everything = _uniprops.ASCII_RANGE if self.is_bytes else _uniprops.UNICODE_RANGE
|
|
719
|
+
if in_group:
|
|
720
|
+
if negated:
|
|
721
|
+
if not v:
|
|
722
|
+
# The inverse of a property that matches nothing is everything, which case-insensitive
|
|
723
|
+
# matching can't extend, so it can be a member of the set.
|
|
724
|
+
return [everything]
|
|
725
|
+
# Added when the set is complete.
|
|
726
|
+
self.negated_members.append(v)
|
|
727
|
+
return ['']
|
|
728
|
+
return [v]
|
|
729
|
+
if negated:
|
|
730
|
+
return [f'[^{v}]' if v else f'[{everything}]']
|
|
731
|
+
return [f'[{v}]' if v else f'[^{everything}]']
|
|
580
732
|
|
|
581
733
|
def main_group(self, i: _util.StringIter) -> list[str]:
|
|
582
734
|
"""The main group: group 0."""
|
|
@@ -1183,7 +1335,7 @@ class _ReplaceParser(Generic[AnyStr]):
|
|
|
1183
1335
|
count = len(self.span_stack)
|
|
1184
1336
|
self.end_found = False
|
|
1185
1337
|
try:
|
|
1186
|
-
while
|
|
1338
|
+
while True:
|
|
1187
1339
|
t = next(i)
|
|
1188
1340
|
if self.use_format and t in _CURLY_BRACKETS:
|
|
1189
1341
|
self.handle_format(t, i)
|
|
@@ -1236,10 +1388,9 @@ class _ReplaceParser(Generic[AnyStr]):
|
|
|
1236
1388
|
except StopIteration:
|
|
1237
1389
|
self.result.append(t)
|
|
1238
1390
|
raise
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
self.result.append(self.convert_case(t, this_case))
|
|
1391
|
+
else:
|
|
1392
|
+
# The stack always has the case just added.
|
|
1393
|
+
self.result.append(self.convert_case(t, cast(int, self.get_single_stack())))
|
|
1243
1394
|
except StopIteration:
|
|
1244
1395
|
pass
|
|
1245
1396
|
|
|
@@ -33,8 +33,16 @@ _LETTERS_UNDERSCORE = _ASCII_LETTERS | frozenset(('_',))
|
|
|
33
33
|
_WORD = _LETTERS_UNDERSCORE | _DIGIT
|
|
34
34
|
_STANDARD_ESCAPES = frozenset(('a', 'b', 'f', 'n', 'r', 't', 'v'))
|
|
35
35
|
_CURLY_BRACKETS = frozenset(('{', '}'))
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# Character set grammar, as defined by Regex.
|
|
37
|
+
_ALNUM = _ASCII_LETTERS | _DIGIT
|
|
38
|
+
_PROPERTY_NAME = _ALNUM | frozenset((' ', '&', '_', '-', '.'))
|
|
39
|
+
_PROPERTY_VALUE = _ALNUM | frozenset((' ', '&', '_', '-', '.', '/'))
|
|
40
|
+
_NAMED_CHAR = _ALNUM | frozenset((' ', '-'))
|
|
41
|
+
_HEX_DIGITS = _HEX | frozenset(c.upper() for c in _HEX)
|
|
42
|
+
_HEX_ESCAPES = {'x': 2, 'u': 4, 'U': 8}
|
|
43
|
+
_SHORT_PROPERTIES = frozenset(('C', 'L', 'M', 'N', 'P', 'S', 'Z'))
|
|
44
|
+
_CLASS_ESCAPES = frozenset(('d', 'D', 'h', 's', 'S', 'w', 'W'))
|
|
45
|
+
_SET_OPS = frozenset(('||', '~~', '&&', '--'))
|
|
38
46
|
_GLOBAL_FLAGS = frozenset(('b', 'e', 'p', 'r', 'V'))
|
|
39
47
|
_SCOPED_FLAGS = frozenset(('a', 'f', 'i', 'L', 'm', 's', 'u', 'w', 'x'))
|
|
40
48
|
_VERSIONS = frozenset(('0', '1'))
|
|
@@ -72,6 +80,7 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
72
80
|
version: int
|
|
73
81
|
is_bytes: bool
|
|
74
82
|
search: AnyStr
|
|
83
|
+
text: str
|
|
75
84
|
|
|
76
85
|
def __init__(self, search: AnyStr, re_verbose: bool = False, re_version: int = 0) -> None:
|
|
77
86
|
"""Initialize."""
|
|
@@ -167,37 +176,6 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
167
176
|
current.extend(["\\", t])
|
|
168
177
|
return current
|
|
169
178
|
|
|
170
|
-
def get_posix(self, i: _util.StringIter) -> str | None:
|
|
171
|
-
"""Get POSIX."""
|
|
172
|
-
|
|
173
|
-
index = i.index
|
|
174
|
-
value = ['[']
|
|
175
|
-
try:
|
|
176
|
-
c = next(i)
|
|
177
|
-
if c != ':':
|
|
178
|
-
raise PatternError('Not a valid property!')
|
|
179
|
-
else:
|
|
180
|
-
value.append(c)
|
|
181
|
-
c = next(i)
|
|
182
|
-
if c == '^':
|
|
183
|
-
value.append(c)
|
|
184
|
-
c = next(i)
|
|
185
|
-
while c != ':':
|
|
186
|
-
if c not in _PROPERTY:
|
|
187
|
-
raise PatternError('Not a valid property!')
|
|
188
|
-
if c not in _PROPERTY_STRIP:
|
|
189
|
-
value.append(c)
|
|
190
|
-
c = next(i)
|
|
191
|
-
value.append(c)
|
|
192
|
-
c = next(i)
|
|
193
|
-
if c != ']' or not value:
|
|
194
|
-
raise PatternError('Unmatched ]')
|
|
195
|
-
value.append(c)
|
|
196
|
-
except Exception:
|
|
197
|
-
i.rewind(i.index - index)
|
|
198
|
-
value = []
|
|
199
|
-
return ''.join(value) if value else None
|
|
200
|
-
|
|
201
179
|
def get_comments(self, i: _util.StringIter) -> str | None:
|
|
202
180
|
"""Get comments."""
|
|
203
181
|
|
|
@@ -342,69 +320,175 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
342
320
|
current.append(t)
|
|
343
321
|
return current
|
|
344
322
|
|
|
323
|
+
@staticmethod
|
|
324
|
+
def peek(i: _util.StringIter, count: int = 1) -> str:
|
|
325
|
+
"""Get up to the next `count` characters without consuming them."""
|
|
326
|
+
|
|
327
|
+
chars = []
|
|
328
|
+
for _ in range(count):
|
|
329
|
+
try:
|
|
330
|
+
chars.append(next(i))
|
|
331
|
+
except StopIteration:
|
|
332
|
+
break
|
|
333
|
+
i.rewind(len(chars))
|
|
334
|
+
return ''.join(chars)
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def match(i: _util.StringIter, text: str) -> bool:
|
|
338
|
+
"""Consume `text` if it is next."""
|
|
339
|
+
|
|
340
|
+
index = i.index
|
|
341
|
+
for c in text:
|
|
342
|
+
try:
|
|
343
|
+
if next(i) == c:
|
|
344
|
+
continue
|
|
345
|
+
except StopIteration:
|
|
346
|
+
pass
|
|
347
|
+
i.rewind(i.index - index)
|
|
348
|
+
return False
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def consume(i: _util.StringIter, chars: frozenset[str], limit: int = -1) -> str:
|
|
353
|
+
"""Consume characters while they are in `chars`, up to `limit` characters if given."""
|
|
354
|
+
|
|
355
|
+
value = []
|
|
356
|
+
while limit:
|
|
357
|
+
try:
|
|
358
|
+
c = next(i)
|
|
359
|
+
except StopIteration:
|
|
360
|
+
break
|
|
361
|
+
if c not in chars:
|
|
362
|
+
i.rewind(1)
|
|
363
|
+
break
|
|
364
|
+
value.append(c)
|
|
365
|
+
limit -= 1
|
|
366
|
+
return ''.join(value)
|
|
367
|
+
|
|
345
368
|
def char_groups(self, t: str, i: _util.StringIter) -> list[str]:
|
|
346
369
|
"""Handle character groups."""
|
|
347
370
|
|
|
348
|
-
|
|
349
|
-
pos = i.index - 1
|
|
350
|
-
found = 0
|
|
351
|
-
sub_first = 0
|
|
352
|
-
escaped = False
|
|
353
|
-
first = 0
|
|
354
|
-
|
|
371
|
+
start = i.index - 1
|
|
355
372
|
try:
|
|
356
|
-
|
|
357
|
-
if not escaped and t == "\\":
|
|
358
|
-
escaped = True
|
|
359
|
-
elif escaped:
|
|
360
|
-
escaped = False
|
|
361
|
-
current.extend(self.reference(t, i, True))
|
|
362
|
-
elif t == "[" and not found:
|
|
363
|
-
found += 1
|
|
364
|
-
first = pos
|
|
365
|
-
current.append(t)
|
|
366
|
-
elif t == "[" and found and self.version == _regex.V1:
|
|
367
|
-
# Start of sub char set found
|
|
368
|
-
posix = None if self.is_bytes else self.get_posix(i)
|
|
369
|
-
if posix:
|
|
370
|
-
current.append(posix)
|
|
371
|
-
pos = i.index - 2
|
|
372
|
-
else:
|
|
373
|
-
found += 1
|
|
374
|
-
sub_first = pos
|
|
375
|
-
current.append(t)
|
|
376
|
-
elif t == "[":
|
|
377
|
-
posix = None if self.is_bytes else self.get_posix(i)
|
|
378
|
-
if posix:
|
|
379
|
-
current.append(posix)
|
|
380
|
-
pos = i.index - 2
|
|
381
|
-
else:
|
|
382
|
-
current.append(t)
|
|
383
|
-
elif t == "^" and found == 1 and (pos == first + 1):
|
|
384
|
-
# Found ^ at start of first char set; adjust 1st char position
|
|
385
|
-
current.append(t)
|
|
386
|
-
first = pos
|
|
387
|
-
elif self.version == _regex.V1 and t == "^" and found > 1 and (pos == sub_first + 1):
|
|
388
|
-
# Found ^ at start of sub char set; adjust 1st char sub position
|
|
389
|
-
current.append(t)
|
|
390
|
-
sub_first = pos
|
|
391
|
-
elif t == "]" and found == 1 and (pos != first + 1):
|
|
392
|
-
# First char set closed; log range
|
|
393
|
-
current.append(t)
|
|
394
|
-
found = 0
|
|
395
|
-
break
|
|
396
|
-
elif self.version == _regex.V1 and t == "]" and found > 1 and (pos != sub_first + 1):
|
|
397
|
-
# Sub char set closed; decrement depth counter
|
|
398
|
-
found -= 1
|
|
399
|
-
current.append(t)
|
|
400
|
-
else:
|
|
401
|
-
current.append(t)
|
|
402
|
-
pos += 1
|
|
403
|
-
t = next(i)
|
|
373
|
+
self.parse_set(i)
|
|
404
374
|
except StopIteration as e:
|
|
405
|
-
raise PatternError(f"Missing ']', unterminated character set at position {
|
|
375
|
+
raise PatternError(f"Missing ']', unterminated character set at position {start}") from e
|
|
376
|
+
|
|
377
|
+
return [self.text[start:i.index]]
|
|
378
|
+
|
|
379
|
+
def parse_set(self, i: _util.StringIter) -> None:
|
|
380
|
+
"""Parse a character set, starting after its opening `[`, through its closing `]`."""
|
|
381
|
+
|
|
382
|
+
self.match(i, '^')
|
|
383
|
+
while True:
|
|
384
|
+
self.parse_set_operand(i)
|
|
385
|
+
if self.version == _regex.V1 and self.peek(i, 2) in _SET_OPS:
|
|
386
|
+
next(i)
|
|
387
|
+
next(i)
|
|
388
|
+
continue
|
|
389
|
+
# The operand only ends at an operator or `]`.
|
|
390
|
+
next(i)
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
def parse_set_operand(self, i: _util.StringIter) -> None:
|
|
394
|
+
"""Parse the members of a set (or of an operand in a set operation) up to an operator or `]`."""
|
|
395
|
+
|
|
396
|
+
# The first member is always a member, even if it is `]`.
|
|
397
|
+
self.parse_set_member(i)
|
|
398
|
+
while True:
|
|
399
|
+
c = self.peek(i, 2)
|
|
400
|
+
if not c:
|
|
401
|
+
raise StopIteration
|
|
402
|
+
if c[0] == ']' or (self.version == _regex.V1 and c in _SET_OPS):
|
|
403
|
+
return
|
|
404
|
+
self.parse_set_member(i)
|
|
405
|
+
|
|
406
|
+
def parse_set_member(self, i: _util.StringIter) -> None:
|
|
407
|
+
"""Parse a member of a character set, which may be a range."""
|
|
408
|
+
|
|
409
|
+
if not self.parse_set_item(i) or self.peek(i) != '-':
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
after = self.peek(i, 2)
|
|
413
|
+
if self.version == _regex.V1 and after == '--':
|
|
414
|
+
# A set difference, not a range.
|
|
415
|
+
return
|
|
416
|
+
next(i)
|
|
417
|
+
if after == '-]':
|
|
418
|
+
# A literal `-` at the end of the set.
|
|
419
|
+
return
|
|
420
|
+
self.parse_set_item(i)
|
|
421
|
+
|
|
422
|
+
def parse_set_item(self, i: _util.StringIter) -> bool:
|
|
423
|
+
"""Parse a single item of a character set, and return whether it is a single character."""
|
|
406
424
|
|
|
407
|
-
|
|
425
|
+
c = next(i)
|
|
426
|
+
if c == '\\':
|
|
427
|
+
return self.parse_set_escape(i)
|
|
428
|
+
if c == '[':
|
|
429
|
+
if self.parse_posix(i):
|
|
430
|
+
return False
|
|
431
|
+
if self.version == _regex.V1:
|
|
432
|
+
# A nested set.
|
|
433
|
+
self.parse_set(i)
|
|
434
|
+
return False
|
|
435
|
+
return True
|
|
436
|
+
|
|
437
|
+
def parse_set_escape(self, i: _util.StringIter) -> bool:
|
|
438
|
+
"""Parse an escape in a character set, and return whether it is a single character."""
|
|
439
|
+
|
|
440
|
+
c = next(i)
|
|
441
|
+
if c in _HEX_ESCAPES:
|
|
442
|
+
self.consume(i, _HEX_DIGITS, _HEX_ESCAPES[c])
|
|
443
|
+
elif c == 'N':
|
|
444
|
+
# A named character, or otherwise a literal `N`.
|
|
445
|
+
index = i.index
|
|
446
|
+
if self.match(i, '{'):
|
|
447
|
+
self.consume(i, _NAMED_CHAR)
|
|
448
|
+
if self.match(i, '}'):
|
|
449
|
+
return True
|
|
450
|
+
i.rewind(i.index - index)
|
|
451
|
+
elif c in ('p', 'P'):
|
|
452
|
+
index = i.index
|
|
453
|
+
if self.match(i, '{'):
|
|
454
|
+
self.match(i, '^')
|
|
455
|
+
self.parse_property_name(i)
|
|
456
|
+
if self.match(i, '}'):
|
|
457
|
+
return False
|
|
458
|
+
# Not a property, so a literal `p` or `P`.
|
|
459
|
+
i.rewind(i.index - index)
|
|
460
|
+
elif self.peek(i) in _SHORT_PROPERTIES:
|
|
461
|
+
next(i)
|
|
462
|
+
return False
|
|
463
|
+
elif c in _CLASS_ESCAPES:
|
|
464
|
+
return False
|
|
465
|
+
elif c in _DIGIT:
|
|
466
|
+
self.consume(i, _OCTAL, 2)
|
|
467
|
+
return True
|
|
468
|
+
|
|
469
|
+
def parse_posix(self, i: _util.StringIter) -> bool:
|
|
470
|
+
"""Parse a POSIX class after an opening `[`, or consume nothing if it isn't one."""
|
|
471
|
+
|
|
472
|
+
index = i.index
|
|
473
|
+
if self.match(i, ':'):
|
|
474
|
+
self.match(i, '^')
|
|
475
|
+
self.parse_property_name(i)
|
|
476
|
+
if self.match(i, ':]'):
|
|
477
|
+
return True
|
|
478
|
+
i.rewind(i.index - index)
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
def parse_property_name(self, i: _util.StringIter) -> None:
|
|
482
|
+
"""Parse a property name, which may be qualified with a value: `name`, `name=value`, or `name:value`."""
|
|
483
|
+
|
|
484
|
+
self.consume(i, _PROPERTY_NAME)
|
|
485
|
+
index = i.index
|
|
486
|
+
if self.peek(i) in (':', '='):
|
|
487
|
+
next(i)
|
|
488
|
+
if self.consume(i, _PROPERTY_VALUE).strip():
|
|
489
|
+
return
|
|
490
|
+
# No value, so the name is unqualified.
|
|
491
|
+
i.rewind(i.index - index)
|
|
408
492
|
|
|
409
493
|
def normal(self, t: str, i: _util.StringIter) -> list[str]:
|
|
410
494
|
"""Handle normal chars."""
|
|
@@ -445,7 +529,8 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
445
529
|
self.verbose = bool(self.re_verbose)
|
|
446
530
|
self.version = self.re_version if self.re_version else _regex.DEFAULT_VERSION
|
|
447
531
|
new_pattern = []
|
|
448
|
-
|
|
532
|
+
self.text = self.process_quotes(search)
|
|
533
|
+
i = _util.StringIter(self.text)
|
|
449
534
|
new_pattern = self.main_group(i)
|
|
450
535
|
return "".join(new_pattern)
|
|
451
536
|
|
|
@@ -267,8 +267,9 @@ class Bre(_util.Immutable, Generic[AnyStr]):
|
|
|
267
267
|
if isinstance(template, ReplaceTemplate):
|
|
268
268
|
if use_format != template.use_format:
|
|
269
269
|
raise ValueError("Compiled replace cannot be a format object!")
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
return self.compile(template)
|
|
271
|
+
elif isinstance(template, (str, bytes)) and self.auto_compile:
|
|
272
|
+
return self.compile(template, (FORMAT if use_format else 0))
|
|
272
273
|
elif isinstance(template, (str, bytes)) and use_format:
|
|
273
274
|
# Reject an attempt to run format replace when auto-compiling
|
|
274
275
|
# of template strings has been disabled and we are using a
|