| # |
| # Secret Labs' Regular Expression Engine |
| # |
| # support for Unicode property escapes \p{...} and \P{...} |
| # |
| # See https://unicode.org/reports/tr18/ "Unicode Regular Expressions", |
| # requirement RL1.2 "Properties". |
| # |
| # The supported properties are matched either as CATEGORY opcodes, or as fixed |
| # sets of character ranges: |
| # |
| # * Properties emitted as CATEGORY opcodes (see _CATEGORY_PROPERTIES): \d, \s |
| # and \w (as digit, space and word, honouring the ASCII/LOCALE/UNICODE |
| # flags), the binary properties Alphabetic, Lowercase, Uppercase, Numeric, |
| # Printable, alnum, XID_Start, XID_Continue, Cased and Case_Ignorable, and |
| # the POSIX classes blank, graph, print and assigned. |
| # |
| # * General_Category values (see _GC_CATEGORY): L, Lt, Nd, Lu, N, Lm, Nl, No, |
| # Cf, Z, Zs, C and Cn (combinations of the simple predicates), plus Cc, Cs, |
| # Co, Zl and Zp as fixed ranges (see _GC_ANALYTIC). |
| # |
| # * Code-point classes given by fixed ranges (see _analytic_ranges): ASCII, |
| # Any, Noncharacter_Code_Point, Join_Control, Regional_Indicator, xdigit, |
| # ASCII_Hex_Digit, Hex_Digit, cntrl, and the immutable Pattern_Syntax and |
| # Pattern_White_Space. |
| # |
| |
| from ._constants import ( |
| IN, CATEGORY, NEGATE, RANGE, LITERAL, |
| CATEGORY_DIGIT, CATEGORY_NOT_DIGIT, |
| CATEGORY_SPACE, CATEGORY_NOT_SPACE, |
| CATEGORY_WORD, CATEGORY_NOT_WORD, |
| CATEGORY_ALPHA, CATEGORY_NOT_ALPHA, |
| CATEGORY_LOWER, CATEGORY_NOT_LOWER, |
| CATEGORY_UPPER, CATEGORY_NOT_UPPER, |
| CATEGORY_NUMERIC, CATEGORY_NOT_NUMERIC, |
| CATEGORY_PRINTABLE, CATEGORY_NOT_PRINTABLE, |
| CATEGORY_ALNUM, CATEGORY_NOT_ALNUM, |
| CATEGORY_XID_START, CATEGORY_NOT_XID_START, |
| CATEGORY_XID_CONTINUE, CATEGORY_NOT_XID_CONTINUE, |
| CATEGORY_TITLE, CATEGORY_NOT_TITLE, |
| CATEGORY_CASED, CATEGORY_NOT_CASED, |
| CATEGORY_CASE_IGNORABLE, CATEGORY_NOT_CASE_IGNORABLE, |
| CATEGORY_LU, CATEGORY_NOT_LU, |
| CATEGORY_N, CATEGORY_NOT_N, |
| CATEGORY_LM, CATEGORY_NOT_LM, |
| CATEGORY_NL, CATEGORY_NOT_NL, |
| CATEGORY_NO, CATEGORY_NOT_NO, |
| CATEGORY_CF, CATEGORY_NOT_CF, |
| CATEGORY_Z, CATEGORY_NOT_Z, |
| CATEGORY_ZS, CATEGORY_NOT_ZS, |
| CATEGORY_C, CATEGORY_NOT_C, |
| CATEGORY_CN, CATEGORY_NOT_CN, |
| CATEGORY_ASSIGNED, CATEGORY_NOT_ASSIGNED, |
| CATEGORY_BLANK, CATEGORY_NOT_BLANK, |
| CATEGORY_GRAPH, CATEGORY_NOT_GRAPH, |
| CATEGORY_PRINT, CATEGORY_NOT_PRINT, |
| ) |
| |
| MAXUNICODE = 0x10FFFF |
| |
| # Properties implemented directly by the engine as (positive, negative) |
| # CATEGORY codes. The keys are normalised (see _normalize). digit, space and |
| # word reuse the \d, \s and \w categories and so are affected by the ASCII, |
| # LOCALE and UNICODE flags; the rest are plain Unicode properties and are not. |
| _CATEGORY_PROPERTIES = { |
| "digit": (CATEGORY_DIGIT, CATEGORY_NOT_DIGIT), # same as \d |
| "space": (CATEGORY_SPACE, CATEGORY_NOT_SPACE), # same as \s |
| # \p{White_Space} is approximated by \s (str.isspace), which also matches |
| # the information separators U+001C..U+001F. |
| "whitespace": (CATEGORY_SPACE, CATEGORY_NOT_SPACE), |
| "wspace": (CATEGORY_SPACE, CATEGORY_NOT_SPACE), |
| "word": (CATEGORY_WORD, CATEGORY_NOT_WORD), # same as \w |
| |
| "alphabetic": (CATEGORY_ALPHA, CATEGORY_NOT_ALPHA), |
| "alpha": (CATEGORY_ALPHA, CATEGORY_NOT_ALPHA), # POSIX |
| "lowercase": (CATEGORY_LOWER, CATEGORY_NOT_LOWER), |
| "lower": (CATEGORY_LOWER, CATEGORY_NOT_LOWER), # POSIX |
| "uppercase": (CATEGORY_UPPER, CATEGORY_NOT_UPPER), |
| "upper": (CATEGORY_UPPER, CATEGORY_NOT_UPPER), # POSIX |
| "numeric": (CATEGORY_NUMERIC, CATEGORY_NOT_NUMERIC), |
| "printable": (CATEGORY_PRINTABLE, CATEGORY_NOT_PRINTABLE), |
| "cased": (CATEGORY_CASED, CATEGORY_NOT_CASED), |
| "caseignorable": (CATEGORY_CASE_IGNORABLE, CATEGORY_NOT_CASE_IGNORABLE), |
| # POSIX classes, the compatibility properties of UTS #18 Annex C (see the |
| # compound predicates in sre.c). |
| "blank": (CATEGORY_BLANK, CATEGORY_NOT_BLANK), |
| "graph": (CATEGORY_GRAPH, CATEGORY_NOT_GRAPH), |
| "print": (CATEGORY_PRINT, CATEGORY_NOT_PRINT), |
| "assigned": (CATEGORY_ASSIGNED, CATEGORY_NOT_ASSIGNED), |
| "alnum": (CATEGORY_ALNUM, CATEGORY_NOT_ALNUM), # POSIX |
| "xidstart": (CATEGORY_XID_START, CATEGORY_NOT_XID_START), |
| "xids": (CATEGORY_XID_START, CATEGORY_NOT_XID_START), |
| "xidcontinue": (CATEGORY_XID_CONTINUE, CATEGORY_NOT_XID_CONTINUE), |
| "xidc": (CATEGORY_XID_CONTINUE, CATEGORY_NOT_XID_CONTINUE), |
| } |
| |
| # General_Category values matched by an engine category. CATEGORY_ALPHA |
| # matches exactly the L group, and CATEGORY_TITLE the Lt category; |
| # CATEGORY_DIGIT matches Nd (but, like \d, is restricted to ASCII under the |
| # ASCII flag). The gc group memberships (L = Lu|Ll|Lt|Lm|Lo, N = Nd|Nl|No) |
| # are given by the Unicode Standard 4.5, Table 4-4 "General_Category Values" |
| # (https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-4/#G124142) |
| # and listed in |
| # https://www.unicode.org/Public/UCD/latest/ucd/PropertyValueAliases.txt |
| # The compound categories Lu, N, Lm, Nl, No, Cf, Z, Zs, C and Cn are |
| # combinations of the simple predicates (see sre.c) that reproduce the |
| # canonical gc partition; they are not Unicode-published identities. |
| _GC_CATEGORY = { |
| "l": (CATEGORY_ALPHA, CATEGORY_NOT_ALPHA), |
| "lt": (CATEGORY_TITLE, CATEGORY_NOT_TITLE), |
| "nd": (CATEGORY_DIGIT, CATEGORY_NOT_DIGIT), |
| "lu": (CATEGORY_LU, CATEGORY_NOT_LU), |
| "n": (CATEGORY_N, CATEGORY_NOT_N), |
| "lm": (CATEGORY_LM, CATEGORY_NOT_LM), |
| "nl": (CATEGORY_NL, CATEGORY_NOT_NL), |
| "no": (CATEGORY_NO, CATEGORY_NOT_NO), |
| "cf": (CATEGORY_CF, CATEGORY_NOT_CF), |
| "z": (CATEGORY_Z, CATEGORY_NOT_Z), |
| "zs": (CATEGORY_ZS, CATEGORY_NOT_ZS), |
| "c": (CATEGORY_C, CATEGORY_NOT_C), |
| "cn": (CATEGORY_CN, CATEGORY_NOT_CN), |
| } |
| |
| # General_Category values whose members are fixed in every Unicode version, |
| # so they need no table: Cc (control, = POSIX cntrl), Cs (surrogates), Co |
| # (private use) and the single code points Zl and Zp. Cc, Cs and Co are the |
| # control codes, surrogate and private-use areas, fixed by the Unicode |
| # Standard 23.1, 23.6 and 23.5: |
| # https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-23/ |
| # All five are listed in |
| # https://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedGeneralCategory.txt |
| _CC_RANGES = [(0x00, 0x1F), (0x7F, 0x9F)] |
| _CS_RANGES = [(0xD800, 0xDFFF)] |
| _CO_RANGES = [(0xE000, 0xF8FF), (0xF0000, 0xFFFFD), (0x100000, 0x10FFFD)] |
| _GC_ANALYTIC = { |
| "cc": _CC_RANGES, |
| "cs": _CS_RANGES, |
| "co": _CO_RANGES, |
| "zl": [(0x2028, 0x2028)], |
| "zp": [(0x2029, 0x2029)], |
| } |
| |
| # Pattern_Syntax and Pattern_White_Space are guaranteed immutable by the |
| # Unicode stability policy, so their members can be hardcoded. |
| # UAX #31 1.1, "Stability": https://www.unicode.org/reports/tr31/ |
| # Members listed in https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt |
| _PATTERN_WHITE_SPACE_RANGES = [ |
| (0x0009, 0x000D), (0x0020, 0x0020), (0x0085, 0x0085), (0x200E, 0x200F), |
| (0x2028, 0x2029), |
| ] |
| _PATTERN_SYNTAX_RANGES = [ |
| (0x0021, 0x002F), (0x003A, 0x0040), (0x005B, 0x005E), (0x0060, 0x0060), |
| (0x007B, 0x007E), (0x00A1, 0x00A7), (0x00A9, 0x00A9), (0x00AB, 0x00AC), |
| (0x00AE, 0x00AE), (0x00B0, 0x00B1), (0x00B6, 0x00B6), (0x00BB, 0x00BB), |
| (0x00BF, 0x00BF), (0x00D7, 0x00D7), (0x00F7, 0x00F7), (0x2010, 0x2027), |
| (0x2030, 0x203E), (0x2041, 0x2053), (0x2055, 0x205E), (0x2190, 0x245F), |
| (0x2500, 0x2775), (0x2794, 0x2BFF), (0x2E00, 0x2E7F), (0x3001, 0x3003), |
| (0x3008, 0x3020), (0x3030, 0x3030), (0xFD3E, 0xFD3F), (0xFE45, 0xFE46), |
| ] |
| |
| # Normalised property names that introduce a General_Category value. A bare |
| # \p{Lu} is shorthand for \p{gc=Lu} (UTS #18 1.2.4, "Property Syntax"). |
| _GC_KEYS = frozenset({"gc", "generalcategory"}) |
| |
| # Normalised value names for the truth value of a binary property; Yes/No and |
| # True/False are the binary value aliases of PropertyValueAliases.txt. |
| _TRUE_VALUES = frozenset({"yes", "y", "true", "t"}) |
| _FALSE_VALUES = frozenset({"no", "n", "false", "f"}) |
| |
| |
| def _analytic_ranges(): |
| # Properties whose members follow directly from the code point. Keys are |
| # normalised. |
| # Noncharacter_Code_Point: U+FDD0..FDEF and the last two of every plane, |
| # permanently reserved (the Unicode Standard 23.7, "Noncharacters": |
| # https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-23/). |
| noncharacter = [(0xFDD0, 0xFDEF)] |
| noncharacter += [(plane | 0xFFFE, plane | 0xFFFF) |
| for plane in range(0, MAXUNICODE + 1, 0x10000)] |
| # Regional_Indicator (RI): the 26 enclosed symbols A..Z, a complete fixed |
| # block (PropList.txt binary property). |
| regional_indicator = [(0x1F1E6, 0x1F1FF)] |
| # ASCII_Hex_Digit (= POSIX xdigit) and Hex_Digit, which adds the fullwidth |
| # forms. Both are complete, fixed sets (PropList.txt binary properties). |
| ascii_hex = [(0x30, 0x39), (0x41, 0x46), (0x61, 0x66)] |
| hex_digit = ascii_hex + [(0xFF10, 0xFF19), (0xFF21, 0xFF26), (0xFF41, 0xFF46)] |
| return { |
| "ascii": [(0, 0x7F)], |
| "any": [(0, MAXUNICODE)], |
| # Join_Control (U+200C ZWNJ, U+200D ZWJ; the Unicode Standard 23.2, |
| # "Layout Controls"), a PropList.txt binary property. |
| "joincontrol": [(0x200C, 0x200D)], |
| "regionalindicator": regional_indicator, |
| "ri": regional_indicator, |
| "noncharactercodepoint": noncharacter, |
| "xdigit": ascii_hex, # POSIX, ASCII only |
| "asciihexdigit": ascii_hex, |
| "ahex": ascii_hex, |
| "hexdigit": hex_digit, |
| "hex": hex_digit, |
| # POSIX cntrl is the General_Category Cc, a fixed set of code points. |
| "cntrl": _CC_RANGES, |
| "patternwhitespace": _PATTERN_WHITE_SPACE_RANGES, |
| "patws": _PATTERN_WHITE_SPACE_RANGES, |
| "patternsyntax": _PATTERN_SYNTAX_RANGES, |
| "patsyn": _PATTERN_SYNTAX_RANGES, |
| } |
| |
| |
| def _normalize(name): |
| # Unicode property and value names are matched loosely: case, spaces, |
| # hyphens and underscores are not significant, and an initial "is" prefix |
| # is ignored (UAX #44 5.9, "Matching Rules", UAX44-LM3; |
| # https://www.unicode.org/reports/tr44/). |
| name = name.lower().replace("_", "").replace("-", "").replace(" ", "") |
| # Strip a leading "is", unless "is" is the whole name and so not a prefix |
| # (e.g. the Line_Break value lb=IS). |
| if name != "is": |
| name = name.removeprefix("is") |
| return name |
| |
| |
| def _from_ranges(ranges, negate): |
| if ranges is None: |
| return None |
| items = [(LITERAL, lo) if lo == hi else (RANGE, (lo, hi)) |
| for lo, hi in ranges] |
| if negate: |
| items.insert(0, (NEGATE, None)) |
| return (IN, items) |
| |
| |
| def _general_category(value, negate): |
| # Resolve a General_Category value to a subpattern using an engine category |
| # or a fixed range set; unsupported values return None. |
| cat = _GC_CATEGORY.get(value) |
| if cat is not None: |
| return (IN, [(CATEGORY, cat[1] if negate else cat[0])]) |
| return _from_ranges(_GC_ANALYTIC.get(value), negate) |
| |
| |
| def _truth(value): |
| value = _normalize(value) |
| if value in _TRUE_VALUES: |
| return True |
| if value in _FALSE_VALUES: |
| return False |
| return None |
| |
| |
| def parse_property(name, negate): |
| """Parse the text inside \\p{...} / \\P{...}. |
| |
| Return an (IN, items) subpattern, or None if the property is unknown. |
| """ |
| prop, sep, value = name.partition("=") |
| if sep: |
| key = _normalize(prop) |
| if key in _GC_KEYS: |
| return _general_category(_normalize(value), negate) |
| # A binary property spelled name=yes or name=no. |
| truth = _truth(value) |
| if truth is None: |
| return None |
| negate ^= not truth |
| cat = _CATEGORY_PROPERTIES.get(key) |
| if cat is not None: |
| return (IN, [(CATEGORY, cat[1] if negate else cat[0])]) |
| return _from_ranges(_analytic_ranges().get(key), negate) |
| |
| key = _normalize(name) |
| cat = _CATEGORY_PROPERTIES.get(key) |
| if cat is not None: |
| return (IN, [(CATEGORY, cat[1] if negate else cat[0])]) |
| ranges = _analytic_ranges().get(key) |
| if ranges is not None: |
| return _from_ranges(ranges, negate) |
| return _general_category(key, negate) |