diff --git a/transcrypt/development/automated_tests/re/basic_pyre.py b/transcrypt/development/automated_tests/re/basic_pyre.py index 05d433a2..3cba84b6 100644 --- a/transcrypt/development/automated_tests/re/basic_pyre.py +++ b/transcrypt/development/automated_tests/re/basic_pyre.py @@ -25,6 +25,8 @@ def run (test): checkSearchWithGroups(test) checkMatchOps(test) checkMatchWithGroups(test) + checkGroupsInCharClass(test) + checkClosingBracketInCharClass(test) # checkMatchWithNamedGroups(test) # !!! @JdeH temporarily disabled this checkFullMatchOps(test) checkFindAllOps(test) diff --git a/transcrypt/development/automated_tests/re/basictests.py b/transcrypt/development/automated_tests/re/basictests.py index 88d8bdfa..25602290 100644 --- a/transcrypt/development/automated_tests/re/basictests.py +++ b/transcrypt/development/automated_tests/re/basictests.py @@ -235,6 +235,43 @@ def checkMatchWithNamedGroups(test, flags = 0): m = r.match("adfs;") test.check(m) +def checkGroupsInCharClass(test, flags = 0): + """ Parentheses inside a character class don't open a capture group + """ + r = re.compile(r"(?P[a-z()]+) = (?P[\d()]+)", flags) + test.check(r.groups) + d = r.groupindex + __pragma__('skip') + d = convertMappingDict(d) + __pragma__('noskip') + test.check( d ) + + m = r.search("a(b)c = 4(2)") + test.check( m.groups() ) + test.check( m.group("key") ) + test.check( m.group("value") ) + + r = re.compile(r"[(](\w+)[)][^()]*", flags) + test.check(r.groups) + test.check( r.search("(abc)def").groups() ) + +def checkClosingBracketInCharClass(test, flags = 0): + """ A ']' right behind the '[' or its negating '^' is a literal one + """ + r = re.compile(r"[]x]+", flags) + test.check( r.findall("]x]a") ) + + r = re.compile(r"[^]x]+", flags) + test.check( r.findall("]x]ab") ) + + r = re.compile(r"a[]](\w)", flags) + test.check(r.groups) + test.check( r.search("a]b").groups() ) + + # without a second one the class stays unterminated + test.check(test.expectException( lambda: re.compile(r"[]", flags) )) + test.check(test.expectException( lambda: re.compile(r"[^]", flags) )) + def checkMatchWithGroups(test, flags = 0): rgx = re.compile(r'(\w)(\w)(\w)?', flags) test.check(rgx.pattern) diff --git a/transcrypt/modules/re/translate.py b/transcrypt/modules/re/translate.py index d3a6da02..22eed9b7 100644 --- a/transcrypt/modules/re/translate.py +++ b/transcrypt/modules/re/translate.py @@ -15,6 +15,9 @@ stringFlags = 'aiLmsux' +# A '[' directly behind one of these tokens is a literal one +LITERAL_BRACKET_TOKENS = ('\\', '(?#') + # Represents a regex group (e.g /()/, /(?:)/ /(?=), etc). # `start` and `end` is the index of the groups start and end token in the token list. class Group: @@ -162,11 +165,40 @@ def resolve(self): return self.name + paras +# Translates the character class up to the closing ']' of an already shifted '['. +# Returns the class as a string and the queue with the remaining characters. +# An unterminated class is consumed as a whole, leaving the error to JS. +def translateCharClass(queue): + idx = 0 + while idx < len(queue): + if queue[idx] == '\\': + idx += 2 + # '[]]' and '[^]]' are valid literals within a Python character class, + # JS lacks that rule and would end the class at this ']' + elif queue[idx] == ']' and (idx == 0 or (idx == 1 and queue[0] == '^')): + queue.insert(idx, '\\') + idx += 2 + elif queue[idx] == ']': + idx += 1 + break + else: + idx += 1 + + return ''.join(queue[:idx]), queue[idx:] + + def shift(stack, queue): done = not bool(queue) if not done: - stack.append(Token(queue[0], [], True)) + char = queue[0] queue = queue[1:] + prevTokenName = stack[len(stack) - 1].name if stack else '' + # A character class becomes a single token, its content is no regex + if char == '[' and prevTokenName not in LITERAL_BRACKET_TOKENS: + charClass, queue = translateCharClass(queue) + stack.append(Token('[' + charClass)) + else: + stack.append(Token(char, [], True)) return stack, queue, done @@ -218,9 +250,6 @@ def shiftReduce(stack, queue, namedGroups, flags): stack = stack[:-1] - elif s1.name == '[' and s0.name == '^': - stack[-2:] = [Token('[^')] - elif s1.name == '(' and s0.name == '?': stack[-2:] = [Token('(?')]