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

WIP: Add background properties #74

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
20 changes: 19 additions & 1 deletion 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_cursor, is_integer, is_length, is_number,
is_percentage, is_quote, is_rect)
is_percentage, is_position, is_quote, is_rect, is_uri)


class Choices:
Expand Down Expand Up @@ -350,9 +350,27 @@ def value(self, context):
BACKGROUND_COLOR_CHOICES = Choices(default, TRANSPARENT, validators=[is_color])

# background_image
# TODO: tests fail if INITIAL is not used, but INITIAL does not seem to be a valid value
BACKGROUND_IMAGE_CHOICES = Choices(None, validators=[is_uri], explicit_defaulting_constants=[INHERIT, INITIAL])

# background_repeat
REPEAT = 'repeat'
REPEAT_X = 'repeat-x'
REPEAT_Y = 'repeat-y'
NO_REPEAT = 'no-repeat'

BACKGROUND_REPEAT_CHOICES = Choices(REPEAT, REPEAT_X, REPEAT_Y, NO_REPEAT, explicit_defaulting_constants=[INHERIT])

# background_attachment
SCROLL = 'scroll'
FIXED = 'fixed'

# TODO: tests fail if INITIAL is not used, but INITIAL does not seem to be a valid value
BACKGROUND_ATTACHMENT_CHOICES = Choices(SCROLL, FIXED, explicit_defaulting_constants=[INHERIT, INITIAL])

# background_position
BACKGROUND_POSITION_CHOICES = Choices(validators=[is_position], explicit_defaulting_constants=[INHERIT])

# background

######################################################################
Expand Down
47 changes: 26 additions & 21 deletions colosseum/declaration.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
from . import engine as css_engine
from . import parser
from . import engine as css_engine, parser
from .constants import ( # noqa
ALIGN_CONTENT_CHOICES, ALIGN_ITEMS_CHOICES, ALIGN_SELF_CHOICES, AUTO,
BACKGROUND_COLOR_CHOICES, BORDER_COLLAPSE_CHOICES, BORDER_COLOR_CHOICES,
BACKGROUND_ATTACHMENT_CHOICES, BACKGROUND_COLOR_CHOICES,
BACKGROUND_IMAGE_CHOICES, BACKGROUND_POSITION_CHOICES,
BACKGROUND_REPEAT_CHOICES, BORDER_COLLAPSE_CHOICES, BORDER_COLOR_CHOICES,
BORDER_SPACING_CHOICES, BORDER_STYLE_CHOICES, BORDER_WIDTH_CHOICES,
BOX_OFFSET_CHOICES, CAPTION_SIDE_CHOICES, CLEAR_CHOICES, CLIP_CHOICES,
COLOR_CHOICES, DIRECTION_CHOICES, DISPLAY_CHOICES, EMPTY_CELLS_CHOICES,
FLEX_BASIS_CHOICES, FLEX_DIRECTION_CHOICES, FLEX_GROW_CHOICES,
FLEX_SHRINK_CHOICES, FLEX_START, FLEX_WRAP_CHOICES, FLOAT_CHOICES,
GRID_AUTO_CHOICES, GRID_AUTO_FLOW_CHOICES, GRID_GAP_CHOICES,
COLOR_CHOICES, CURSOR_CHOICES, DIRECTION_CHOICES, DISPLAY_CHOICES,
EMPTY_CELLS_CHOICES, FLEX_BASIS_CHOICES, FLEX_DIRECTION_CHOICES,
FLEX_GROW_CHOICES, FLEX_SHRINK_CHOICES, FLEX_START, FLEX_WRAP_CHOICES,
FLOAT_CHOICES, GRID_AUTO_CHOICES, GRID_AUTO_FLOW_CHOICES, GRID_GAP_CHOICES,
GRID_PLACEMENT_CHOICES, GRID_TEMPLATE_AREA_CHOICES, GRID_TEMPLATE_CHOICES,
INITIAL, INLINE, INVERT, JUSTIFY_CONTENT_CHOICES, LETTER_SPACING_CHOICES,
LTR, MARGIN_CHOICES, MAX_SIZE_CHOICES, MEDIUM, MIN_SIZE_CHOICES, NORMAL,
NOWRAP, ORDER_CHOICES, ORPHANS_CHOICES, OUTLINE_COLOR_CHOICES,
OUTLINE_STYLE_CHOICES, OUTLINE_WIDTH_CHOICES, OVERFLOW_CHOICES,
PADDING_CHOICES, PAGE_BREAK_AFTER_CHOICES, PAGE_BREAK_BEFORE_CHOICES,
PAGE_BREAK_INSIDE_CHOICES, POSITION_CHOICES, QUOTES_CHOICES, ROW,
SEPARATE, SHOW, SIZE_CHOICES, STATIC, STRETCH, TABLE_LAYOUT_CHOICES,
TEXT_ALIGN_CHOICES, TEXT_DECORATION_CHOICES, TEXT_INDENT_CHOICES,
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, CURSOR_CHOICES,
PAGE_BREAK_INSIDE_CHOICES, POSITION_CHOICES, QUOTES_CHOICES, REPEAT, ROW,
SCROLL, SEPARATE, SHOW, SIZE_CHOICES, STATIC, STRETCH,
TABLE_LAYOUT_CHOICES, TEXT_ALIGN_CHOICES, TEXT_DECORATION_CHOICES,
TEXT_INDENT_CHOICES, 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,
)
from .exceptions import ValidationError
from .wrappers import Border, BorderBottom, BorderLeft, BorderRight, BorderTop, Outline
from .wrappers import (
Background, Border, BorderBottom, BorderLeft, BorderRight, BorderTop,
Outline,
)

_CSS_PROPERTIES = set()

Expand Down Expand Up @@ -112,7 +116,6 @@ def validated_property(name, choices, initial):
# Check the value attribute is a callable
if not callable(value_attr):
raise ValueError('Initial value "%s" `value` attribute is not callable!' % initial)

except AttributeError:
raise ValueError('Initial value "%s" does not have a value attribute!' % initial)

Expand Down Expand Up @@ -350,11 +353,13 @@ def __init__(self, **style):

# 14.2.1 Background properties
background_color = validated_property('background_color', choices=BACKGROUND_COLOR_CHOICES, initial=default)
# background_image
# background_repeat
# background_attachment
# background_position
# background
background_image = validated_property('background_image', choices=BACKGROUND_IMAGE_CHOICES, initial=None)
background_repeat = validated_property('background_repeat', choices=BACKGROUND_REPEAT_CHOICES, initial=REPEAT)
background_attachment = validated_property('background_attachment', choices=BACKGROUND_ATTACHMENT_CHOICES,
initial=SCROLL)
background_position = validated_property('background_position', choices=BACKGROUND_POSITION_CHOICES,
initial=('0%', '0%'))
background = validated_shorthand_property('background', parser=parser.background, wrapper=Background)

# 15. Fonts ##########################################################
# 15.3 Font family
Expand Down
135 changes: 134 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, Cursor, Quotes, Uri
from .wrappers import BorderSpacing, Cursor, Position, Quotes, Uri


def units(value):
Expand Down Expand Up @@ -445,3 +445,136 @@ def cursor(values):
raise ValueError('Value {value} is not a valid cursor value'.format(value=value))

return Cursor(validated_values)


##############################################################################
# Background
##############################################################################
def position(value):
"""
[[ <percentage> | <length> | left | center | right ][ <percentage> | <length> | top | center | bottom ]? ] |
[[ left | center | right ] || [ top | center | bottom ]]

Reference:
- https://www.w3.org/TR/2011/REC-CSS2-20110607/colors.html#background-properties
"""
from .constants import ( # noqa
BOTTOM, CENTER, LEFT, RIGHT, TOP,
)

if value:
if isinstance(value, str):
values = [val.strip() for val in value.split()]
elif isinstance(value, Sequence):
values = value
else:
raise ValueError('Unknown position %s ' % value)
else:
raise ValueError('Unknown position %s ' % value)

if len(values) == 1:
# If only one value is specified, the second value is assumed to be 'center'.
# If at least one value is not a keyword, then the first value represents the horizontal
# position and the second represents the vertical position. Negative <percentage> and
# <length> values are allowed.
try:
return Position(horizontal=units(values[0]))
except ValueError:
if values[0] in [LEFT, RIGHT, CENTER]:
return Position(horizontal=values[0])

if values[0] in [TOP, BOTTOM]:
return Position(vertical=values[0])

elif len(values) == 2:
horizontal, vertical = None, None

# Check first value
try:
horizontal = units(values[0])
except ValueError:
if values[0] in [LEFT, CENTER, RIGHT]:
horizontal = values[0]

if values[0] in [TOP, CENTER, BOTTOM]:
vertical = values[0]

# Check second value
try:
vertical = units(values[1])
except ValueError:
if values[1] in [LEFT, CENTER, RIGHT]:
horizontal = values[1]

if values[1] in [TOP, CENTER, BOTTOM]:
vertical = values[1]

return Position(horizontal=horizontal, vertical=vertical)

raise ValueError('Position contains too many parts!')


##############################################################################
# Background shorthand
##############################################################################
def _parse_background_property_part(value, background_dict):
"""Parse background shorthand property part for known properties."""
from .constants import ( # noqa
BACKGROUND_ATTACHMENT_CHOICES, BACKGROUND_COLOR_CHOICES, BACKGROUND_IMAGE_CHOICES,
BACKGROUND_POSITION_CHOICES, BACKGROUND_REPEAT_CHOICES
)

for property_name, choices in {'background_color': BACKGROUND_COLOR_CHOICES,
'background_image': BACKGROUND_IMAGE_CHOICES,
'background_repeat': BACKGROUND_REPEAT_CHOICES,
'background_attachment': BACKGROUND_ATTACHMENT_CHOICES,
'background_position': BACKGROUND_POSITION_CHOICES}.items():
try:
value = choices.validate(value)
except (ValueError, ValidationError):
continue

if property_name in background_dict:
raise ValueError('Invalid duplicated property!')

background_dict[property_name] = value
return background_dict

raise ValueError('Background value "{value}" not valid!'.format(value=value))


def background(value):
"""
Parse background string into a dictionary of background properties.

The font CSS property is a shorthand for background-color, background-image,
background-repeat, background-attachment, and background-position.

Reference:
- https://www.w3.org/TR/2011/REC-CSS2-20110607/colors.html#background-properties
"""
if value:
if isinstance(value, str):
values = [val.strip() for val in value.split()]
elif isinstance(value, Sequence):
values = value
else:
raise ValueError('Unknown background %s ' % value)
else:
raise ValueError('Unknown background %s ' % value)

# We iteratively split by the first left hand space found and try to validate if that part
# is a valid <background-color> or <background-image> or <background-repeat> or
# <background-attachment> or <background-position> (which can come in any order)

# We us this dictionary to store parsed values and check that values properties are not
# duplicated
background_dict = {}
for idx, part in enumerate(values):
if idx > 2:
# Outline can have a maximum of 3 parts
raise ValueError('Background property shorthand contains too many parts!')

background_dict = _parse_background_property_part(part, background_dict)

return background_dict
15 changes: 15 additions & 0 deletions colosseum/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,18 @@ def is_cursor(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 ] ]')


def is_position(value):
"""Check if given value is of content quotes and return it."""
try:
value = parser.position(value)
except ValueError:
raise ValidationError('Value {value} is not a valid position'.format(value=value))

return value


is_position.description = ('[ [ <percentage> | <length> | left | center | right ] '
'[ <percentage> | <length> | top | center | bottom ]? ] | '
'[ [ left | center | right ] || [ top | center | bottom ] ]')
50 changes: 50 additions & 0 deletions colosseum/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,51 @@ def vertical(self):
return self._horizontal if self._vertical is None else self._vertical


class Position:
"""
Position wrapper.

Examples:
Position(1px)
Position(1px, 2px)
Position('center', 2px)
Position('center', 'top')
"""

def __init__(self, horizontal=None, vertical=None):
self._horizontal = horizontal
self._vertical = vertical

def __repr__(self):
pass
# if self._vertical is None:
# string = 'Position({horizontal})'.format(horizontal=repr(self._horizontal))
# else:
# string = 'Position({horizontal}, {vertical})'.format(horizontal=repr(self._horizontal),
# vertical=repr(self._vertical))
# return string

def __str__(self):
pass
# if self._vertical is not None:
# string = '{horizontal} {vertical}'.format(horizontal=self._horizontal,
# vertical=self._vertical)
# else:
# string = '{horizontal}'.format(horizontal=self._horizontal)

# return string

@property
def horizontal(self):
"""Return the horizontal position."""
return 'center' if self._horizontal is None else self._horizontal

@property
def vertical(self):
"""Return the vertical position."""
return 'center' if self._vertical is None else self._vertical


class Quotes:
"""
Content opening and closing quotes wrapper.
Expand Down Expand Up @@ -153,6 +198,11 @@ class Border(Shorthand):
VALID_KEYS = ['border_width', 'border_style', 'border_color']


class Background(Shorthand):
VALID_KEYS = ['background_color', 'background_image', 'background_repeat', 'background_attachment',
'background_position']


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

Expand Down