diff --git a/colosseum/constants.py b/colosseum/constants.py index e5a45e5fc..3db655de9 100644 --- a/colosseum/constants.py +++ b/colosseum/constants.py @@ -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: @@ -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 ###################################################################### diff --git a/colosseum/declaration.py b/colosseum/declaration.py index 40320cbb8..c81cf918b 100644 --- a/colosseum/declaration.py +++ b/colosseum/declaration.py @@ -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() @@ -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) @@ -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 diff --git a/colosseum/parser.py b/colosseum/parser.py index 85b7acb8a..347fd4a73 100644 --- a/colosseum/parser.py +++ b/colosseum/parser.py @@ -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): @@ -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): + """ + [[ | | left | center | right ][ | | 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 and + # 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 or or or + # or (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 diff --git a/colosseum/validators.py b/colosseum/validators.py index 7ba8ec978..12cb57673 100644 --- a/colosseum/validators.py +++ b/colosseum/validators.py @@ -189,3 +189,18 @@ def is_cursor(value): is_cursor.description = ('[ [ ,]* [ 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 = ('[ [ | | left | center | right ] ' + '[ | | top | center | bottom ]? ] | ' + '[ [ left | center | right ] || [ top | center | bottom ] ]') diff --git a/colosseum/wrappers.py b/colosseum/wrappers.py index 05809ff1c..66104d4ba 100644 --- a/colosseum/wrappers.py +++ b/colosseum/wrappers.py @@ -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. @@ -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."""