adjust parsing to raise nicer exceptions
parent
fcb648b9bb
commit
21ca2fe6a8
|
@ -43,8 +43,15 @@ except NameError:
|
||||||
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
||||||
UPPERCASE = set('MZLHVCSQTA')
|
UPPERCASE = set('MZLHVCSQTA')
|
||||||
|
|
||||||
COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])")
|
TOKEN_RE = re.compile(r"""
|
||||||
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
(
|
||||||
|
[MmZzLlHhVvCcSsQqTtAa] # command
|
||||||
|
|
|
||||||
|
(?:[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?) # float
|
||||||
|
)
|
||||||
|
""", re.VERBOSE
|
||||||
|
)
|
||||||
|
SEPARATORS = ', \t\r\n'
|
||||||
|
|
||||||
# Default Parameters ##########################################################
|
# Default Parameters ##########################################################
|
||||||
|
|
||||||
|
@ -3179,19 +3186,28 @@ class Path(MutableSequence):
|
||||||
next(b, None)
|
next(b, None)
|
||||||
return zip(a, b)
|
return zip(a, b)
|
||||||
|
|
||||||
def _tokenize_path(self, pathdef):
|
@staticmethod
|
||||||
for x in COMMAND_RE.split(pathdef):
|
def _tokenize_path(*pathdef_lines):
|
||||||
if x in COMMANDS:
|
# yield line number and offset in addition to token
|
||||||
yield x
|
# so we can raise syntax errors
|
||||||
for token in FLOAT_RE.findall(x):
|
for lineno, line in enumerate(pathdef_lines):
|
||||||
yield token
|
start = 0
|
||||||
|
for token in TOKEN_RE.split(line):
|
||||||
|
if token.strip(SEPARATORS):
|
||||||
|
yield token, lineno, start
|
||||||
|
start += len(token)
|
||||||
|
|
||||||
def _parse_path(self, pathdef, current_pos=0j, tree_element=None):
|
def _parse_path(self, pathdef, current_pos=0j, tree_element=None):
|
||||||
# In the SVG specs, initial movetos are absolute, even if
|
# In the SVG specs, initial movetos are absolute, even if
|
||||||
# specified as 'm'. This is the default behavior here as well.
|
# specified as 'm'. This is the default behavior here as well.
|
||||||
# But if you pass in a current_pos variable, the initial moveto
|
# But if you pass in a current_pos variable, the initial moveto
|
||||||
# will be relative to that current_pos. This is useful.
|
# will be relative to that current_pos. This is useful.
|
||||||
elements = list(self._tokenize_path(pathdef))
|
|
||||||
|
# we need to keep the pathdef split by lines so we can retrieve a specific line
|
||||||
|
# to throw syntax errors
|
||||||
|
pathdef_lines = pathdef.splitlines()
|
||||||
|
|
||||||
|
elements = list(self._tokenize_path(*pathdef_lines))
|
||||||
# Reverse for easy use of .pop()
|
# Reverse for easy use of .pop()
|
||||||
elements.reverse()
|
elements.reverse()
|
||||||
|
|
||||||
|
@ -3200,27 +3216,43 @@ class Path(MutableSequence):
|
||||||
start_pos = None
|
start_pos = None
|
||||||
command = None
|
command = None
|
||||||
|
|
||||||
while elements:
|
def pop_float(elements):
|
||||||
|
try:
|
||||||
|
token, lineno, start = elements.pop()
|
||||||
|
try:
|
||||||
|
return float(token)
|
||||||
|
except ValueError:
|
||||||
|
line = pathdef_lines[lineno]
|
||||||
|
end = start + len(token)
|
||||||
|
raise self._syntax_error('invalid token %r' % token, lineno, line, start, end)
|
||||||
|
except IndexError:
|
||||||
|
lineno = len(pathdef_lines) - 1
|
||||||
|
line = pathdef_lines[lineno]
|
||||||
|
end = len(line) - 1
|
||||||
|
raise self._syntax_error('not enough arguments', lineno, line, end, end)
|
||||||
|
|
||||||
if elements[-1] in COMMANDS:
|
while elements:
|
||||||
|
if elements[-1][0] in COMMANDS:
|
||||||
# New command.
|
# New command.
|
||||||
last_command = command # Used by S and T
|
last_command = command # Used by S and T
|
||||||
command = elements.pop()
|
command = elements.pop()[0]
|
||||||
absolute = command in UPPERCASE
|
absolute = command in UPPERCASE
|
||||||
command = command.upper()
|
command = command.upper()
|
||||||
else:
|
else:
|
||||||
# If this element starts with numbers, it is an implicit command
|
# If this element starts with numbers, it is an implicit command
|
||||||
# and we don't change the command. Check that it's allowed:
|
# and we don't change the command. Check that it's allowed:
|
||||||
if command is None:
|
if command is None:
|
||||||
raise ValueError("Unallowed implicit command in %s, position %s" % (
|
token, lineno, start = elements[-1]
|
||||||
pathdef, len(pathdef.split()) - len(elements)))
|
end = start + len(token)
|
||||||
|
line = pathdef_lines[lineno]
|
||||||
|
raise self._syntax_error("missing command", lineno, line, start, end)
|
||||||
last_command = command # Used by S and T
|
last_command = command # Used by S and T
|
||||||
|
|
||||||
if command == 'M':
|
if command == 'M':
|
||||||
# Moveto command.
|
# Moveto command.
|
||||||
x = elements.pop()
|
x = pop_float(elements)
|
||||||
y = elements.pop()
|
y = pop_float(elements)
|
||||||
pos = float(x) + float(y) * 1j
|
pos = x + y * 1j
|
||||||
if absolute:
|
if absolute:
|
||||||
current_pos = pos
|
current_pos = pos
|
||||||
else:
|
else:
|
||||||
|
@ -3245,34 +3277,34 @@ class Path(MutableSequence):
|
||||||
command = None
|
command = None
|
||||||
|
|
||||||
elif command == 'L':
|
elif command == 'L':
|
||||||
x = elements.pop()
|
x = pop_float(elements)
|
||||||
y = elements.pop()
|
y = pop_float(elements)
|
||||||
pos = float(x) + float(y) * 1j
|
pos = x + y * 1j
|
||||||
if not absolute:
|
if not absolute:
|
||||||
pos += current_pos
|
pos += current_pos
|
||||||
segments.append(Line(current_pos, pos))
|
segments.append(Line(current_pos, pos))
|
||||||
current_pos = pos
|
current_pos = pos
|
||||||
|
|
||||||
elif command == 'H':
|
elif command == 'H':
|
||||||
x = elements.pop()
|
x = pop_float(elements)
|
||||||
pos = float(x) + current_pos.imag * 1j
|
pos = x + current_pos.imag * 1j
|
||||||
if not absolute:
|
if not absolute:
|
||||||
pos += current_pos.real
|
pos += current_pos.real
|
||||||
segments.append(Line(current_pos, pos))
|
segments.append(Line(current_pos, pos))
|
||||||
current_pos = pos
|
current_pos = pos
|
||||||
|
|
||||||
elif command == 'V':
|
elif command == 'V':
|
||||||
y = elements.pop()
|
y = pop_float(elements)
|
||||||
pos = current_pos.real + float(y) * 1j
|
pos = current_pos.real + y * 1j
|
||||||
if not absolute:
|
if not absolute:
|
||||||
pos += current_pos.imag * 1j
|
pos += current_pos.imag * 1j
|
||||||
segments.append(Line(current_pos, pos))
|
segments.append(Line(current_pos, pos))
|
||||||
current_pos = pos
|
current_pos = pos
|
||||||
|
|
||||||
elif command == 'C':
|
elif command == 'C':
|
||||||
control1 = float(elements.pop()) + float(elements.pop()) * 1j
|
control1 = pop_float(elements) + pop_float(elements) * 1j
|
||||||
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
control2 = pop_float(elements) + pop_float(elements) * 1j
|
||||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
end = pop_float(elements) + pop_float(elements) * 1j
|
||||||
|
|
||||||
if not absolute:
|
if not absolute:
|
||||||
control1 += current_pos
|
control1 += current_pos
|
||||||
|
@ -3297,8 +3329,8 @@ class Path(MutableSequence):
|
||||||
# to the current point.
|
# to the current point.
|
||||||
control1 = current_pos + current_pos - segments[-1].control2
|
control1 = current_pos + current_pos - segments[-1].control2
|
||||||
|
|
||||||
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
control2 = pop_float(elements) + pop_float(elements) * 1j
|
||||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
end = pop_float(elements) + pop_float(elements) * 1j
|
||||||
|
|
||||||
if not absolute:
|
if not absolute:
|
||||||
control2 += current_pos
|
control2 += current_pos
|
||||||
|
@ -3308,8 +3340,8 @@ class Path(MutableSequence):
|
||||||
current_pos = end
|
current_pos = end
|
||||||
|
|
||||||
elif command == 'Q':
|
elif command == 'Q':
|
||||||
control = float(elements.pop()) + float(elements.pop()) * 1j
|
control = pop_float(elements) + pop_float(elements) * 1j
|
||||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
end = pop_float(elements) + pop_float(elements) * 1j
|
||||||
|
|
||||||
if not absolute:
|
if not absolute:
|
||||||
control += current_pos
|
control += current_pos
|
||||||
|
@ -3333,7 +3365,7 @@ class Path(MutableSequence):
|
||||||
# to the current point.
|
# to the current point.
|
||||||
control = current_pos + current_pos - segments[-1].control
|
control = current_pos + current_pos - segments[-1].control
|
||||||
|
|
||||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
end = pop_float(elements) + pop_float(elements) * 1j
|
||||||
|
|
||||||
if not absolute:
|
if not absolute:
|
||||||
end += current_pos
|
end += current_pos
|
||||||
|
@ -3343,11 +3375,11 @@ class Path(MutableSequence):
|
||||||
|
|
||||||
elif command == 'A':
|
elif command == 'A':
|
||||||
|
|
||||||
radius = float(elements.pop()) + float(elements.pop()) * 1j
|
radius = pop_float(elements) + pop_float(elements) * 1j
|
||||||
rotation = float(elements.pop())
|
rotation = pop_float(elements)
|
||||||
arc = float(elements.pop())
|
arc = pop_float(elements)
|
||||||
sweep = float(elements.pop())
|
sweep = pop_float(elements)
|
||||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
end = pop_float(elements) + pop_float(elements) * 1j
|
||||||
|
|
||||||
if not absolute:
|
if not absolute:
|
||||||
end += current_pos
|
end += current_pos
|
||||||
|
@ -3369,3 +3401,12 @@ class Path(MutableSequence):
|
||||||
current_pos = end
|
current_pos = end
|
||||||
|
|
||||||
return segments
|
return segments
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _syntax_error(cls, msg, lineno, line, offset,end_offset):
|
||||||
|
filename = '<svg-d-string>'
|
||||||
|
try:
|
||||||
|
return SyntaxError(msg, (filename, lineno+1, offset + 1, line, lineno, end_offset + 1))
|
||||||
|
except IndexError:
|
||||||
|
# on python < 3.10
|
||||||
|
return SyntaxError(msg, (filename, lineno+1, offset + 1, line))
|
||||||
|
|
|
@ -178,10 +178,20 @@ class TestParser(unittest.TestCase):
|
||||||
path2 = Path(Line(-3.4e+38 + 3.4e+38j, -3.4e-38 + 3.4e-38j))
|
path2 = Path(Line(-3.4e+38 + 3.4e+38j, -3.4e-38 + 3.4e-38j))
|
||||||
self.assertEqual(path1, path2)
|
self.assertEqual(path1, path2)
|
||||||
|
|
||||||
def test_errors(self):
|
def test_error_missing_command(self):
|
||||||
self.assertRaises(ValueError, parse_path,
|
with self.assertRaises(SyntaxError) as e:
|
||||||
'M 100 100 L 200 200 Z 100 200')
|
parse_path('M 100 100 L 200 200 Z 100 200')
|
||||||
|
assert "missing command" in e.exception.msg
|
||||||
|
|
||||||
|
def test_error_invalid_token(self):
|
||||||
|
with self.assertRaises(SyntaxError) as e:
|
||||||
|
parse_path("M 0 \n1 N 2 3")
|
||||||
|
assert "invalid token" in e.exception.msg
|
||||||
|
|
||||||
|
def test_error_not_enough_arguments(self):
|
||||||
|
with self.assertRaises(SyntaxError) as e:
|
||||||
|
Path("M 0 1\n 2")
|
||||||
|
assert "not enough arguments" in e.exception.msg
|
||||||
|
|
||||||
def test_transform(self):
|
def test_transform(self):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue