Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Add cursor property #56

Merged
merged 10 commits into from
Apr 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions colosseum/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .validators import (ValidationError, is_border_spacing, is_color,
is_integer, is_length, is_number, is_percentage,
is_quote, is_rect)
is_cursor, is_integer, is_length, is_number,
is_percentage, is_quote, is_rect)


class Choices:
Expand Down Expand Up @@ -509,6 +509,30 @@ def value(self, context):
# 18.1 Cursors
# cursor

AUTO = 'auto'
CROSSHAIR = 'crosshair'
DEFAULT = 'default'
POINTER = 'pointer'
MOVE = 'move'
E_RESIZE = 'e-resize'
NE_RESIZE = 'ne-resize'
NW_RESIZE = 'nw-resize'
N_RESIZE = 'n-resize'
SE_RESIZE = 'se-resize'
SW_RESIZE = 'sw-resize'
S_RESIZE = 's-resize'
W_RESIZE = 'w-resize'
TEXT = 'text'
WAIT = 'wait'
PROGRESS = 'progress'
HELP = 'help'

CURSOR_OPTIONS = [AUTO, CROSSHAIR, DEFAULT, POINTER, MOVE, E_RESIZE, NE_RESIZE, NW_RESIZE, N_RESIZE, SE_RESIZE,
SW_RESIZE, S_RESIZE, W_RESIZE, TEXT, WAIT, PROGRESS, HELP]

# Since the order is important, the cursor options are used in the is_cursor validator
CURSOR_CHOICES = Choices(validators=[is_cursor], explicit_defaulting_constants=[INHERIT])

######################################################################
# 18.4 Dynamic outlines
######################################################################
Expand Down
4 changes: 2 additions & 2 deletions colosseum/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
TEXT_TRANSFORM_CHOICES, TOP, TRANSPARENT, UNICODE_BIDI_CHOICES,
VISIBILITY_CHOICES, VISIBLE, WHITE_SPACE_CHOICES, WIDOWS_CHOICES,
WORD_SPACING_CHOICES, Z_INDEX_CHOICES, OtherProperty,
TextAlignInitialValue, default,
TextAlignInitialValue, default, CURSOR_CHOICES,
)
from .exceptions import ValidationError
from .wrappers import Border, BorderBottom, BorderLeft, BorderRight, BorderTop, Outline
Expand Down Expand Up @@ -410,7 +410,7 @@ def __init__(self, **style):

# 18. User interface #################################################
# 18.1 Cursors
# cursor
cursor = validated_property('cursor', CURSOR_CHOICES, initial=AUTO)

# 18.4 Dynamic outlines
outline_width = validated_property('outline_width', choices=OUTLINE_WIDTH_CHOICES, initial=MEDIUM)
Expand Down
78 changes: 77 additions & 1 deletion colosseum/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .exceptions import ValidationError
from .shapes import Rect
from .units import Unit, px
from .wrappers import BorderSpacing, Quotes
from .wrappers import BorderSpacing, Cursor, Quotes, Uri


def units(value):
Expand Down Expand Up @@ -369,3 +369,79 @@ def border_bottom(value):
def border_top(value):
"""Parse border string into a dictionary of outline properties."""
return border(value, direction='top')


##############################################################################
# Uri
##############################################################################
def uri(value):
"""Parse a url from a value.

Accepts:
* url("<url>")
* url( "<url>" )
* url('<url>')
* url( '<url>' )
* url(<url>)
* url( <url> )
"""
if isinstance(value, str):
value = value.strip()
else:
raise ValueError('Value {value} must be a string')

if value.startswith('url(') and value.endswith(')'):
# Remove the 'url(' and ')'
value = value[4:-1].strip()

# Single quotes and optional spaces
if value.startswith("'") and value.endswith("'"):
return Uri(value[1:-1])
# Double quotes and optional spaces
elif value.startswith('"') and value.endswith('"'):
# '("some.url")'
return Uri(value[1:-1])
# No quotes and optional spaces
else:
return Uri(value)

raise ValueError('Invalid url %s' % value)


##############################################################################
# Cursor
##############################################################################
def cursor(values):
"""Parse a cursor from a value."""
from .constants import CURSOR_OPTIONS

if isinstance(values, str):
values = [val.strip() for val in values.split(',')]

validated_values = []
has_cursor_option = False
option_count = 0
for value in values:
if value in CURSOR_OPTIONS:
has_cursor_option = True
validated_values.append(value)
option_count += 1

if option_count > 1:
raise ValueError('There can only be one cursor option in {values}!'.format(values=values))

continue
else:
if has_cursor_option:
raise ValueError('Values {values} are in incorrect order. '
'Cursor option must come last!'.format(values=values))
try:
value = uri(value)
validated_values.append(value)
continue
except ValueError:
raise ValueError('Value {value} is not a valid url value'.format(value=value))

raise ValueError('Value {value} is not a valid cursor value'.format(value=value))

return Cursor(validated_values)
46 changes: 44 additions & 2 deletions colosseum/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
Validate values of different css properties.
"""

from . import parser
from . import units
import re

from . import parser, units
from .exceptions import ValidationError


Expand Down Expand Up @@ -147,3 +148,44 @@ def is_quote(value):


is_quote.description = '[<string> <string>]+'


URI_RE = re.compile(r"""(
(?:url\(\s?'[A-Za-z0-9\./\:\?]*'\s?\)) # Single quotes and optional spaces
|
(?:url\(\s?"[A-Za-z0-9\./\:\?]*"\s?\)) # Double quotes and optional spaces
|
(?:url\(\s?[A-Za-z0-9\./\:\?]*\s?\)) # No quotes and optional spaces
)""", re.VERBOSE)


def is_uri(value):
"""Validate value is <uri>."""
try:
value = parser.uri(value)
except ValueError as error:
raise ValidationError(str(error))

return value


is_uri.description = '<uri>'


def is_cursor(value):
"""
Validate if values are correct cursor values and in correct order and quantity.

This validator returns a list.
"""
try:
value = parser.cursor(value)
except ValueError as error:
raise ValidationError(str(error))

return value


is_cursor.description = ('[ [<uri> ,]* [ auto | crosshair | default | pointer | move | e-resize '
'| ne-resize | nw-resize | n-resize | se-resize | sw-resize | s-resize '
'| w-resize | text | wait | help | progress ] ]')
84 changes: 84 additions & 0 deletions colosseum/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,87 @@ class BorderLeft(Shorthand):

class Border(Shorthand):
VALID_KEYS = ['border_width', 'border_style', 'border_color']


class Uri:
"""Wrapper for a url."""

def __init__(self, url):
self._url = url

def __repr__(self):
return 'url("%s")' % self._url
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@freakboy3742 what should be the canonical url repr?
I just used 'url("%s")' but it could be

"url('%s')" or 'url(%s)'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the context, I'd say that the quotes, while allowed by the standard, are redundant, so I'd leave them off the canonical form.

Copy link
Contributor Author

@goanpeca goanpeca Jan 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but given:

Some characters appearing in an unquoted URI, such as parentheses, white space characters, single quotes (') and double quotes ("), must be escaped with a backslash so that the resulting URI value is a URI token: '(', ')'.

I felt was easier to simply quote and avoid having to add escapes. Thoughts @freakboy3742 ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - so I can see two alternatives here.

Firstly, we could consider ignoring ", ' and () characters entirely. Have you included these characters because it makes sense to include them, or because the standard actually requires them? Does the URL have to be URL encoded? Are escaped quote characters actually a risk?

If we can't ignore them, we should be formatting with %r instead of %s. That will include the external quotes, but will also autoescape the content of the string, including switching quotes between ' and " such that quoting will be minimized.


def __str__(self):
return repr(self)

@property
def url(self):
return self._url


class ImmutableList(list):
"""Immutable list to store list properties."""

def __init__(self, iterable=()):
super().__init__(iterable)

def _get_error_message(self, err):
return str(err).replace('list', self.__class__.__name__, 1)

# def __eq__(self, other):
# return other.__class__ == self.__class__ and self == other

def __getitem__(self, index):
try:
return super().__getitem__(index)
except Exception as err:
error_msg = self._get_error_message(err)
raise err.__class__(error_msg)

def __setitem__(self, index, value):
raise TypeError("{} values cannot be changed!".format(self.__class__.__name__))

def __hash__(self):
return hash((self.__class__.__name__, tuple(self)))

def __repr__(self):
class_name = self.__class__.__name__
if len(self) != 0:
text = '{class_name}([{data}])'.format(data=repr(list(self))[1:-1], class_name=class_name)
else:
text = '{class_name}()'.format(class_name=class_name)

return text

def __str__(self):
return ', '.join(str(v) for v in self)

def copy(self):
return self.__class__(self)

# Disable mutating methods
def append(self, object):
raise TypeError("{} values cannot be changed!".format(self.__class__.__name__))

def extend(self, iterable):
raise TypeError("{} values cannot be changed!".format(self.__class__.__name__))

def insert(self, index, object):
raise TypeError("{} values cannot be changed!".format(self.__class__.__name__))

def pop(self, index=None):
raise TypeError("{} values cannot be changed!".format(self.__class__.__name__))

def remove(self, value):
raise TypeError("{} values cannot be changed!".format(self.__class__.__name__))

def reverse(self):
raise TypeError("{} values cannot be changed!".format(self.__class__.__name__))

def sort(self, cmp=None, key=None, reverse=False):
raise TypeError("{} values cannot be changed!".format(self.__class__.__name__))


class Cursor(ImmutableList):
"""Immutable list to store cursor property."""
Loading