Skip to content

Commit

Permalink
Update list property and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
goanpeca committed Jan 3, 2020
1 parent f497c6f commit fc14114
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 80 deletions.
8 changes: 4 additions & 4 deletions colosseum/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ def validate(self, value):
raise ValueError()

def __str__(self):
choices = set([str(c).lower().replace('_', '-') for c in self.constants])
choices = [str(c).lower().replace('_', '-') for c in self.constants]
for validator in self.validators:
choices.add(validator.description)
choices += validator.description.split(', ')

if self.explicit_defaulting_constants:
for item in self.explicit_defaulting_constants:
choices.add(item)
choices.append(item)

return ", ".join(sorted(choices))
return ", ".join(sorted(set(choices)))


######################################################################
Expand Down
31 changes: 18 additions & 13 deletions colosseum/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,21 @@

def validated_font_property(name, initial):
"""Define the shorthand CSS font property."""
assert isinstance(initial, dict)
initial = initial.copy()

def getter(self):
font = initial
for property_name, initial_value in font.items():
font[property_name] = getattr(self, '_%s' % property_name, initial_value)
for property_name in font:
font[property_name] = getattr(self, property_name)
return getattr(self, '_%s' % name, construct_font_property(font))

def setter(self, value):
font = parse_font_property(value)
for property_name, property_value in font.items():
setattr(self, '_%s' % property_name, property_value)
setattr(self, property_name, property_value)
self.dirty = True
setattr(self, name, value)
setattr(self, '_%s' % name, value)

def deleter(self):
try:
Expand All @@ -48,7 +50,7 @@ def deleter(self):

for property_name in INITIAL_FONT_VALUES:
try:
delattr(self, '_%s' % property_name)
delattr(self, property_name)
self.dirty = True
except AttributeError:
# Attribute doesn't exist
Expand All @@ -58,29 +60,32 @@ def deleter(self):
return property(getter, setter, deleter)


def validated_list_property(name, choices, initial):
"""Define a property holding a list of comma separated values."""
def validated_list_property(name, choices, initial, separator=','):
"""Define a property holding a list values."""
if not isinstance(initial, list):
raise ValueError('Initial value must be a list!')

def getter(self):
return getattr(self, '_%s' % name, initial)
# TODO: A copy is returned so if the user mutates it,
# it will not affect the stored value
return getattr(self, '_%s' % name, initial)[:]

def setter(self, value):
try:
if not isinstance(value, str):
value = ', '.join(value)
value = separator.join(value)

value = ' '.join(value.strip().split())
value = choices.validate(value)
values = [v.strip() for v in value.split(',')]
# This should be a list of values
values = choices.validate(value)
if not isinstance(values, list):
values.split(separator)
except ValueError:
raise ValueError("Invalid value '%s' for CSS property '%s'; Valid values are: %s" % (
value, name, choices
))

if values != getattr(self, '_%s' % name, initial):
setattr(self, '_%s' % name, values)
setattr(self, '_%s' % name, values[:])
self.dirty = True

def deleter(self):
Expand Down
102 changes: 51 additions & 51 deletions colosseum/fonts.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import re

from .constants import (FONT_STYLE_CHOICES, FONT_VARIANT_CHOICES,
from .constants import (FONT_FAMILY_CHOICES, FONT_SIZE_CHOICES,
FONT_STYLE_CHOICES, FONT_VARIANT_CHOICES,
FONT_WEIGHT_CHOICES, INHERIT, INITIAL_FONT_VALUES,
NORMAL, SYSTEM_FONT_KEYWORDS)
from .validators import ValidationError

# Find spaces within single and double quotes
SQ_PATTERN = re.compile(r"\s+(?=(?:(?:[^']*'){2})*[^']*'[^']*$)")
DQ_PATTERN = re.compile(r'\s+(?=(?:(?:[^"]*"){2})*[^"]*"[^"]*$)')


def get_system_font(keyword):
"""Return a font object from given system font keyword."""
Expand All @@ -20,76 +15,81 @@ def get_system_font(keyword):

def construct_font_property(font):
"""Construct font property string from a dictionary of font properties."""
font['font_family'] = ', '.join(font['font_family'])
if isinstance(font['font_family'], list):
font['font_family'] = ', '.join(font['font_family'])

return ('{font_style} {font_variant} {font_weight} '
'{font_size}/{line_height} {font_family}').format(**font)


def replace_font_family_spaces(string, space_sep):
"""Replace spaces between quotes by character."""
string = SQ_PATTERN.sub(space_sep, string)
string = DQ_PATTERN.sub(space_sep, string)
return string
def parse_font_property_part(value, font):
"""Parse font shorthand property part for known properties."""
if value != NORMAL:
for property_name, choices in {'font_variant': FONT_VARIANT_CHOICES,
'font_weight': FONT_WEIGHT_CHOICES,
'font_style': FONT_STYLE_CHOICES}.items():
try:
value = choices.validate(value)
font[property_name] = value
return font
except (ValidationError, ValueError):
pass

# Maybe it is a font size
if '/' in value:
font['font_size'], font['line_height'] = value.split('/')
return font
else:
try:
FONT_SIZE_CHOICES.validate(value)
font['font_size'] = value
return font
except ValueError:
pass

raise ValidationError

return font


def parse_font_property(string):
"""
Parse font string into a dictionary of font properties.
Reference:
- https://www.w3.org/TR/CSS22/fonts.html#font-shorthand
- https://www.w3.org/TR/CSS21/fonts.html#font-shorthand
- https://developer.mozilla.org/en-US/docs/Web/CSS/font
"""
font = INITIAL_FONT_VALUES.copy()
comma, space = ', ', ' '
comma_sep, space_sep = '~', '@'

# Remove extra inner spaces
string = space.join(string.strip().split())

# Replace commas between font families with some character
string = string.replace(comma, comma_sep)
string = string.replace(comma[0], comma_sep)
string = replace_font_family_spaces(string, space_sep)
parts = string.split()
# Remove extra spaces
string = ' '.join(string.strip().split())

parts = string.split(' ', 1)
if len(parts) == 1:
value = parts[0]
if value == INHERIT:
# ??
# TODO: ??
pass
else:
if value not in SYSTEM_FONT_KEYWORDS:
error_msg = ('Font property value "{value}" '
'not a system font keyword!'.format(value=value))
raise ValidationError(error_msg)
font = get_system_font(value)
elif len(parts) <= 5:
font_properties, font_family = parts[:-1], parts[-1]

# Restore original space characters
font_family = font_family.replace(comma_sep, comma)
font_family = font_family.replace(space_sep, space)
font['font_family'] = font_family

# font size is be the last item on the rest of font properties
font['font_size'] = font_properties.pop(-1)

for value in font_properties:
if value != NORMAL:
for property_name, choices in {'font_variant': FONT_VARIANT_CHOICES,
'font_weight': FONT_WEIGHT_CHOICES,
'font_style': FONT_STYLE_CHOICES}.items():
try:
value = choices.validate(value)
font[property_name] = value
except (ValidationError, ValueError):
pass

if '/' in font['font_size']:
font['font_size'], font['line_height'] = font['font_size'].split('/')
else:
error_msg = ('Font property shorthand contains too many parts!')
raise ValidationError(error_msg)
for _ in range(5):
value = parts[0]
try:
font = parse_font_property_part(value, font)
parts = parts[-1].split(' ', 1)
except ValidationError:
break
else:
# Font family can have a maximum of 4 parts before the font_family part
raise ValidationError('Font property shorthand contains too many parts!')

value = ' '.join(parts)
font['font_family'] = FONT_FAMILY_CHOICES.validate(value)

return font
19 changes: 17 additions & 2 deletions colosseum/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ def validator(num_value):
if min_value is None and max_value is None:
return validator(value)
else:
validator.description = '<number>'
return validator


is_number.description = '<number>'



def is_integer(value=None, min_value=None, max_value=None):
"""
Validate that value is a valid integer.
Expand All @@ -62,6 +64,7 @@ def validator(num_value):
if min_value is None and max_value is None:
return validator(value)
else:
validator.description = '<integer>'
return validator


Expand Down Expand Up @@ -108,18 +111,29 @@ def is_color(value):
is_color.description = '<color>'


# https://www.w3.org/TR/2011/REC-CSS2-20110607/syndata.html#value-def-identifier
_CSS_IDENTIFIER_RE = re.compile(r'^[a-zA-Z][a-zA-Z0-9\-\_]+$')


def is_font_family(value=None, generic_family=None):
"""Validate that value is a valid font family."""
"""
Validate that value is a valid font family.
This validator returns a list.
"""
generic_family = generic_family or []

def validator(font_value):
font_value = ' '.join(font_value.strip().split())
values = [v.strip() for v in font_value.split(',')]
checked_values = []
for val in values:
# Remove extra inner spaces
val = val.replace('" ', '"')
val = val.replace(' "', '"')
val = val.replace("' ", "'")
val = val.replace(" '", "'")

if (val.startswith('"') and val.endswith('"')
or val.startswith("'") and val.endswith("'")):
# TODO: Check that the font exists?
Expand All @@ -142,11 +156,12 @@ def validator(font_value):
error_msg = 'Invalid font string "{invalid}"'.format(invalid=invalid)
raise ValidationError(error_msg)

return ', '.join(checked_values)
return checked_values

if generic_family is []:
return validator(value)
else:
validator.description = '<family-name>, <generic-family>'
return validator


Expand Down
Loading

0 comments on commit fc14114

Please sign in to comment.