343 lines
11 KiB
Python
343 lines
11 KiB
Python
|
#!/usr/bin/env python
|
||
|
#coding:utf-8
|
||
|
# Author: mozman --<mozman@gmx.at>
|
||
|
# Purpose: typechecker
|
||
|
# Created: 15.10.2010
|
||
|
# Copyright (C) 2010, Manfred Moitzi
|
||
|
# License: MIT License
|
||
|
|
||
|
import re
|
||
|
|
||
|
from svgwrite.data import pattern
|
||
|
from svgwrite.data.colors import colornames
|
||
|
from svgwrite.data.svgparser import is_valid_transferlist, is_valid_pathdata, is_valid_animation_timing
|
||
|
from svgwrite.utils import is_string
|
||
|
|
||
|
def iterflatlist(values):
|
||
|
""" Flatten nested *values*, returns an *iterator*. """
|
||
|
for element in values:
|
||
|
if hasattr(element, "__iter__") and not is_string(element):
|
||
|
for item in iterflatlist(element):
|
||
|
yield item
|
||
|
else:
|
||
|
yield element
|
||
|
|
||
|
INVALID_NAME_CHARS = frozenset([' ', '\t', '\r', '\n', ',', '(', ')'])
|
||
|
WHITESPACE = frozenset([' ', '\t', '\r', '\n'])
|
||
|
SHAPE_PATTERN = re.compile(r"^rect\((.*),(.*),(.*),(.*)\)$")
|
||
|
FUNCIRI_PATTERN = re.compile(r"^url\((.*)\)$")
|
||
|
ICCCOLOR_PATTERN = re.compile(r"^icc-color\((.*)\)$")
|
||
|
COLOR_HEXDIGIT_PATTERN = re.compile(r"^#[a-fA-F0-9]{3}([a-fA-F0-9]{3})?$")
|
||
|
COLOR_RGB_INTEGER_PATTERN = re.compile(r"^rgb\( *\d+ *, *\d+ *, *\d+ *\)$")
|
||
|
COLOR_RGB_PERCENTAGE_PATTERN = re.compile(r"^rgb\( *\d+(\.\d*)?% *, *\d+(\.\d*)?% *, *\d+(\.\d*)?% *\)$")
|
||
|
NMTOKEN_PATTERN = re.compile(r"^[a-zA-Z_:][\w\-\.:]*$")
|
||
|
|
||
|
|
||
|
class Full11TypeChecker(object):
|
||
|
def get_version(self):
|
||
|
return '1.1', 'full'
|
||
|
|
||
|
def is_angle(self, value):
|
||
|
#angle ::= number (~"deg" | ~"grad" | ~"rad")?
|
||
|
if self.is_number(value):
|
||
|
return True
|
||
|
elif is_string(value):
|
||
|
return pattern.angle.match(value.strip()) is not None
|
||
|
return False
|
||
|
|
||
|
def is_anything(self, value):
|
||
|
#anything ::= Char*
|
||
|
return bool(str(value).strip())
|
||
|
is_string = is_anything
|
||
|
is_content_type = is_anything
|
||
|
|
||
|
def is_color(self, value):
|
||
|
#color ::= "#" hexdigit hexdigit hexdigit (hexdigit hexdigit hexdigit)?
|
||
|
# | "rgb(" wsp* integer comma integer comma integer wsp* ")"
|
||
|
# | "rgb(" wsp* number "%" comma number "%" comma number "%" wsp* ")"
|
||
|
# | color-keyword
|
||
|
#hexdigit ::= [0-9A-Fa-f]
|
||
|
#comma ::= wsp* "," wsp*
|
||
|
value = str(value).strip()
|
||
|
if value.startswith('#'):
|
||
|
if COLOR_HEXDIGIT_PATTERN.match(value):
|
||
|
return True
|
||
|
else:
|
||
|
return False
|
||
|
elif value.startswith('rgb('):
|
||
|
if COLOR_RGB_INTEGER_PATTERN.match(value):
|
||
|
return True
|
||
|
elif COLOR_RGB_PERCENTAGE_PATTERN.match(value):
|
||
|
return True
|
||
|
return False
|
||
|
return self.is_color_keyword(value)
|
||
|
|
||
|
def is_color_keyword(self, value):
|
||
|
return value.strip() in colornames
|
||
|
|
||
|
def is_frequency(self, value):
|
||
|
# frequency ::= number (~"Hz" | ~"kHz")
|
||
|
if self.is_number(value):
|
||
|
return True
|
||
|
elif is_string(value):
|
||
|
return pattern.frequency.match(value.strip()) is not None
|
||
|
return False
|
||
|
|
||
|
def is_FuncIRI(self, value):
|
||
|
# FuncIRI ::= "url(" <IRI> ")"
|
||
|
res = FUNCIRI_PATTERN.match(str(value).strip())
|
||
|
if res:
|
||
|
return self.is_IRI(res.group(1))
|
||
|
return False
|
||
|
|
||
|
def is_icccolor(self, value):
|
||
|
# icccolor ::= "icc-color(" name (comma-wsp number)+ ")"
|
||
|
res = ICCCOLOR_PATTERN.match(str(value).strip())
|
||
|
if res:
|
||
|
return self.is_list_of_T(res.group(1), 'name')
|
||
|
return False
|
||
|
|
||
|
def is_integer(self, value):
|
||
|
if isinstance(value, float):
|
||
|
return False
|
||
|
try:
|
||
|
number = int(value)
|
||
|
return True
|
||
|
except:
|
||
|
return False
|
||
|
|
||
|
def is_IRI(self, value):
|
||
|
# Internationalized Resource Identifiers
|
||
|
# a more generalized complement to Uniform Resource Identifiers (URIs)
|
||
|
# nearly everything can be a valid <IRI>
|
||
|
# only a none-empty string ist a valid input
|
||
|
if is_string(value):
|
||
|
return bool(value.strip())
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
def is_length(self, value):
|
||
|
# length ::= number ("em" | "ex" | "px" | "in" | "cm" | "mm" | "pt" | "pc" | "%")?
|
||
|
if value is None:
|
||
|
return False
|
||
|
if isinstance(value, (int, float)):
|
||
|
return self.is_number(value)
|
||
|
elif is_string(value):
|
||
|
result = pattern.length.match(value.strip())
|
||
|
if result:
|
||
|
number, tmp, unit = result.groups()
|
||
|
return self.is_number(number) # for tiny check!
|
||
|
return False
|
||
|
|
||
|
is_coordinate = is_length
|
||
|
|
||
|
def is_list_of_T(self, value, t='string'):
|
||
|
def split(value):
|
||
|
#TODO: improve split function!!!!
|
||
|
if isinstance(value, (int, float)):
|
||
|
return (value, )
|
||
|
if is_string(value):
|
||
|
return iterflatlist(v.split(',') for v in value.split(' '))
|
||
|
return value
|
||
|
#list-of-Ts ::= T
|
||
|
# | T comma-wsp list-of-Ts
|
||
|
#comma-wsp ::= (wsp+ ","? wsp*) | ("," wsp*)
|
||
|
#wsp ::= (#x20 | #x9 | #xD | #xA)
|
||
|
checker = self.get_func_by_name(t)
|
||
|
for v in split(value):
|
||
|
if not checker(v):
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def is_four_numbers(self, value):
|
||
|
def split(value):
|
||
|
if is_string(value):
|
||
|
values = iterflatlist( (v.strip().split(' ') for v in value.split(',')) )
|
||
|
return (v for v in values if v)
|
||
|
else:
|
||
|
return iterflatlist(value)
|
||
|
|
||
|
values = list(split(value))
|
||
|
if len(values) != 4:
|
||
|
return False
|
||
|
checker = self.get_func_by_name('number')
|
||
|
for v in values:
|
||
|
if not checker(v):
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def is_semicolon_list(self, value):
|
||
|
#a semicolon-separated list of values
|
||
|
# | value comma-wsp list-of-values
|
||
|
#comma-wsp ::= (wsp+ ";" wsp*) | ("," wsp*)
|
||
|
#wsp ::= (#x20 | #x9 | #xD | #xA)
|
||
|
return self.is_list_of_T(value.replace(';', ' '), 'string')
|
||
|
|
||
|
def is_name(self, value):
|
||
|
# name ::= [^,()#x20#x9#xD#xA] /* any char except ",", "(", ")" or wsp */
|
||
|
chars = frozenset(str(value).strip())
|
||
|
if not chars or INVALID_NAME_CHARS.intersection(chars):
|
||
|
return False
|
||
|
else:
|
||
|
return True
|
||
|
|
||
|
def is_number(self, value):
|
||
|
try:
|
||
|
number = float(value)
|
||
|
return True
|
||
|
except:
|
||
|
return False
|
||
|
|
||
|
def is_number_optional_number(self, value):
|
||
|
#number-optional-number ::= number
|
||
|
# | number comma-wsp number
|
||
|
if is_string(value):
|
||
|
values = re.split('[ ,]+', value.strip())
|
||
|
if 0 < len(values) < 3: # 1 or 2 numbers
|
||
|
for v in values:
|
||
|
if not self.is_number(v):
|
||
|
return False
|
||
|
return True
|
||
|
else:
|
||
|
try: # is it a 2-tuple
|
||
|
n1, n2 = value
|
||
|
if self.is_number(n1) and \
|
||
|
self.is_number(n2):
|
||
|
return True
|
||
|
except TypeError: # just one value
|
||
|
return self.is_number(value)
|
||
|
except ValueError: # more than 2 values
|
||
|
pass
|
||
|
return False
|
||
|
|
||
|
def is_paint(self, value):
|
||
|
#paint ::= "none" |
|
||
|
# "currentColor" |
|
||
|
# <color> [<icccolor>] |
|
||
|
# <funciri> [ "none" | "currentColor" | <color> [<icccolor>] |
|
||
|
# "inherit"
|
||
|
def split_values(value):
|
||
|
try:
|
||
|
funcIRI, value = value.split(")", 1)
|
||
|
values = [funcIRI+")"]
|
||
|
values.extend(split_values(value))
|
||
|
return values
|
||
|
except ValueError:
|
||
|
return value.split()
|
||
|
|
||
|
values = split_values(str(value).strip())
|
||
|
for value in [v.strip() for v in values]:
|
||
|
if value in ('none', 'currentColor', 'inherit'):
|
||
|
continue
|
||
|
elif self.is_color(value):
|
||
|
continue
|
||
|
elif self.is_icccolor(value):
|
||
|
continue
|
||
|
elif self.is_FuncIRI(value):
|
||
|
continue
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def is_percentage(self, value):
|
||
|
#percentage ::= number "%"
|
||
|
if self.is_number(value):
|
||
|
return True
|
||
|
elif is_string(value):
|
||
|
return pattern.percentage.match(value.strip()) is not None
|
||
|
return False
|
||
|
|
||
|
def is_time(self, value):
|
||
|
#time ::= <number> (~"ms" | ~"s")?
|
||
|
if self.is_number(value):
|
||
|
return True
|
||
|
elif is_string(value):
|
||
|
return pattern.time.match(value.strip()) is not None
|
||
|
return False
|
||
|
|
||
|
def is_transform_list(self, value):
|
||
|
if is_string(value):
|
||
|
return is_valid_transferlist(value)
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
def is_path_data(self, value):
|
||
|
if is_string(value):
|
||
|
return is_valid_pathdata(value)
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
def is_XML_Name(self, value):
|
||
|
# http://www.w3.org/TR/2006/REC-xml-20060816/#NT-Name
|
||
|
# Nmtoken
|
||
|
return bool(NMTOKEN_PATTERN.match(str(value).strip()))
|
||
|
|
||
|
def is_shape(self, value):
|
||
|
# shape ::= (<top> <right> <bottom> <left>)
|
||
|
# where <top>, <bottom> <right>, and <left> specify offsets from the
|
||
|
# respective sides of the box.
|
||
|
# <top>, <right>, <bottom>, and <left> are <length> values
|
||
|
# i.e. 'rect(5px, 10px, 10px, 5px)'
|
||
|
res = SHAPE_PATTERN.match(value.strip())
|
||
|
if res:
|
||
|
for arg in res.groups():
|
||
|
if arg.strip() == 'auto':
|
||
|
continue
|
||
|
if not self.is_length(arg):
|
||
|
return False
|
||
|
else:
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def is_timing_value_list(self, value):
|
||
|
if is_string(value):
|
||
|
return is_valid_animation_timing(value)
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
def is_list_of_text_decoration_style(self, value):
|
||
|
return self.is_list_of_T(value, t='text_decoration_style')
|
||
|
|
||
|
def is_text_decoration_style(self, value):
|
||
|
return value in ('overline', 'underline', 'line-through', 'blink')
|
||
|
|
||
|
def get_func_by_name(self, funcname):
|
||
|
return getattr(self,
|
||
|
'is_'+funcname.replace('-', '_'),
|
||
|
self.is_anything)
|
||
|
|
||
|
def check(self, typename, value):
|
||
|
if typename.startswith('list-of-'):
|
||
|
t = typename[8:]
|
||
|
return self.is_list_of_T(value, t)
|
||
|
return self.get_func_by_name(typename)(value)
|
||
|
|
||
|
|
||
|
FOCUS_CONST = frozenset(['nav-next', 'nav-prev', 'nav-up', 'nav-down', 'nav-left',
|
||
|
'nav-right', 'nav-up-left', 'nav-up-right', 'nav-down-left',
|
||
|
'nav-down-right'])
|
||
|
|
||
|
|
||
|
class Tiny12TypeChecker(Full11TypeChecker):
|
||
|
def get_version(self):
|
||
|
return '1.2', 'tiny'
|
||
|
|
||
|
def is_boolean(self, value):
|
||
|
if isinstance(value, bool):
|
||
|
return True
|
||
|
if is_string(value):
|
||
|
return value.strip().lower() in ('true', 'false')
|
||
|
return False
|
||
|
|
||
|
def is_number(self, value):
|
||
|
try:
|
||
|
number = float(value)
|
||
|
if -32767.9999 <= number <= 32767.9999:
|
||
|
return True
|
||
|
else:
|
||
|
return False
|
||
|
except:
|
||
|
return False
|
||
|
|
||
|
def is_focus(self, value):
|
||
|
return str(value).strip() in FOCUS_CONST
|