svgwrite / svgwrite / data / typechecker.py

#!/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 sys
import re

from svgwrite.data import pattern
from svgwrite.data.colors import colornames
from svgwrite.data.svgparser import TransformListParser, PathDataParser, AnimationTimingParser
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+% *\)$")
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* integer "%" comma integer "%" comma integer "%" 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(';', ' '), 'number')

    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 TransformListParser.is_valid(value)
        else:
            return False

    def is_path_data(self, value):
        if is_string(value):
            return PathDataParser.is_valid(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 AnimationTimingParser.is_valid(value)
        else:
            return False

    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
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.