""" Parser for extracting CSS property names, values, and priorities from set of CSS Class Selectors. These class
selectors are gathered externally by the ``HTMLClassParser()``.
| CSS Unit Reference: http://www.w3schools.com/cssref/css_units.asp
| CSS Value Reference: http://www.w3.org/TR/CSS21/propidx.html
**Parameters:**
| class_set (*set*) -- A set() of potential css properties.
**Returns:** Object of Type ClassPropertyParser
**Examples:**
Give this HTML: ``<div class="super-5 h-16-i padding-2 margin-10">Hello</div>``
The HTMLClassParser() would extract the following ``class_set``.
>>> class_set = {'super-5', 'h-16-i', 'PADDING-2', 'margin-10', }
>>> property_parser = ClassPropertyParser(class_set)
>>> # Note that 'super-5' was removed.
>>> print(property_parser.class_set)
{'h-16-i', 'padding-2', 'margin-10'}
>>> css_class = list(property_parser.class_set)[0]
>>> print(css_class)
h-16-i
>>> name = property_parser.get_property_name(css_class=css_class)
>>> # Decodes 'h-' to 'height'
>>> print(name)
height
>>> # Could throw a ValueError.
>>> # See cssbuilder.py for an example of how to handle it.
>>> encoded_property_value = property_parser.get_encoded_property_value(
>>> property_name=name,
>>> css_class=css_class
>>> )
>>> print(encoded_property_value)
16
>>> priority = property_parser.get_property_priority(css_class=css_class)
>>> print(priority)
IMPORTANT
>>> value = property_parser.get_property_value(
>>> property_name=name,
>>> encoded_property_value=encoded_property_value
>>> )
>>> print(value) # Note the unit conversion from px_to_em.
1em
"""
# python 2
from __future__ import absolute_import
# builtins
from string import ascii_lowercase, digits
from re import findall
# plugins
from cssutils import parseString
# custom
from blowdrycss.datalibrary import ordered_property_dict, property_alias_dict, property_regex_dict, \
pseudo_classes, pseudo_elements
from blowdrycss.utilities import deny_empty_or_whitespace
from blowdrycss.cssvalueparser import CSSPropertyValueParser
__author__ = 'chad nelson'
__project__ = 'blowdrycss'
[docs]class ClassPropertyParser(object):
def __init__(self, class_set=set()):
css = '''/* Generated with blowdrycss. */'''
self.sheet = parseString(css)
self.rules = []
self.importance_designator = '-i' # '-i' is used to designate that the priority level is '!important'
self.pseudo_class = ''
self.pseudo_element = ''
self.removed_class_set = set()
self.class_set = class_set
self.clean_class_set()
# print('clean ran')
[docs] def class_set_to_lowercase(self):
""" Converts member variable self.class_set to lowercase.
:return: None
"""
self.class_set = {css_class.lower() for css_class in self.class_set}
[docs] @staticmethod
def underscores_valid(css_class=''):
"""
Validate underscore usage in a single css_class.
In general, underscores are only allowed to designate a decimal point between two numbers.
**Rules:**
- Strip all whitespace in front and back.
- Underscores are only valid between digits
- ``[0-9]_[0-9]`` allowed
- ``_35`` not allowed
- ``42_`` not allowed
- ``bold_px`` not allowed
- If found in the middle of a string it may begin and/or end with ``-``
- ``1_2-5_75-1_2-5_75`` allowed
- String may start with ``n`` to designate negative numbers.
- ``n5_25cm`` allowed.
- String may not start with ``-``
- ``-7_2`` not allowed.
- String may not end with ``-``
- ``5_4-`` not allowed
:type css_class: str
:param css_class: Accepts a single CSS class extracted from HTML class attribute.
:return: boolean
**True cases:**
>>> ClassPropertyParser.underscores_valid('6_3')
True
>>> ClassPropertyParser.underscores_valid('2_456em')
True
>>> ClassPropertyParser.underscores_valid('1_2-5_75-1_2-5_75')
True
>>> ClassPropertyParser.underscores_valid('n5_25cm-n6_1cm')
True
**False cases:**
>>> ClassPropertyParser.underscores_valid('-7_2')
False
>>> ClassPropertyParser.underscores_valid('5_4-')
False
>>> ClassPropertyParser.underscores_valid('_b')
False
>>> ClassPropertyParser.underscores_valid('b_')
False
>>> ClassPropertyParser.underscores_valid('padding--_2')
False
>>> ClassPropertyParser.underscores_valid('2_rem')
False
>>> ClassPropertyParser.underscores_valid('m_px')
False
>>> ClassPropertyParser.underscores_valid('__')
False
"""
# TODO: Replace with regex.
# The underscore and dash is not allowed to be the first or last character of css_class.
css_class = css_class.strip() # Remove any surrounding whitespace.
if (css_class[0] == '_' or css_class[-1] == '_' or
css_class[0] == '-' or css_class[-1] == '-'):
return False
# Check character before and after underscore index.
index = css_class.find('_')
allowed_before = set(digits)
allowed_after = set(digits)
if index > 0: # Underscore is not the first character.
valid = set(css_class[index-1]) <= allowed_before # Check Character before
valid = valid and set(css_class[index+1]) <= allowed_after # Check Character after.
else:
valid = True
return valid
[docs] def clean_class_set(self):
"""
Detect and Remove invalid css classes from class_set
Class names must abide by: http://www.w3.org/TR/CSS2/syndata.html#characters
For purposes of this project only a SUBSET of the CSS standard is permissible as follows:
- Encoded classes shall not be ``None`` or ``''``.
- Encoded shall not contain whitespace (handled implicitly by subsequent rules).
- Encoded classes are only allowed to begin with ``[a-z]``
- Encoded classes are only allowed to end with ``[a-z0-9]``
- Encoded classes are allowed to contain ``[_a-z0-9-]`` between the first and last characters.
- Underscores are only valid between digits ``[0-9]_[0-9]``
**Reference:**
http://stackoverflow.com/questions/1323364/in-python-how-to-check-if-a-string-only-contains-certain-characters
:return: None
"""
# Normalize class data by setting all strings to lowercase first.
self.class_set_to_lowercase()
# Validate against character sets.
allowed_first = set(ascii_lowercase)
allowed_middle = set(ascii_lowercase + digits + '_-')
allowed_last = set(ascii_lowercase + digits)
# Gather invalid_css_classes
invalid_css_classes = []
reasons = []
# 'continue' is used to prevent the same css_class from being added to the invalid_css_classes multiple times.
for css_class in self.class_set:
if not css_class: # None or ''
invalid_css_classes.append(css_class)
reasons.append(' (May not be None or "".)')
continue
if not set(css_class[0]) <= allowed_first: # First character
invalid_css_classes.append(css_class)
reasons.append(' (Only a-z allowed for first character of class.)')
continue
if not set(css_class) <= allowed_middle: # All characters
invalid_css_classes.append(css_class)
reasons.append(' (Only a-z, 0-9, "_", and "-" are allowed in class name.)')
continue
if not set(css_class[-1]) <= allowed_last: # Last character
invalid_css_classes.append(css_class)
reasons.append(' (Only a-z and 0-9 allowed for last character of class.)')
continue
if not self.underscores_valid(css_class=css_class): # Underscore
invalid_css_classes.append(css_class)
reasons.append(' (Invalid underscore usage in class.)')
continue
# Remove invalid_css_classes from self.class_set
for i, invalid_css_class in enumerate(invalid_css_classes):
self.class_set.remove(invalid_css_class)
self.removed_class_set.add(invalid_css_class + reasons[i])
# Property Name Section
#
[docs] @staticmethod
def get_property_name(css_class=''):
"""
Extract a property name from a given class.
**Rules:**
- Classes that use css property names or aliases must set a property value.
**Valid:**
- ``font-weight-700`` is valid because ``700`` is a valid ``font-weight`` value.
- ``fw-700`` is valid because it ``fw-`` is a valid alias for ``font-weight``
- ``bold`` is valid because the ``bold`` alias implies a property name of ``font-weight``
**Invalid:**
- ``font-weight`` by itself is a property name, but does not include a property value.
- ``fw-`` by itself is a property alias, but does not include a property value.
- ``700`` does imply ``font-weight``, but is not a valid CSS selector as it may not begin with a number.
:type css_class: str
:param css_class: A class name containing a property name and value pair, or just a property value
from which the property name may be inferred.
:return: (str) -- Class returns the property_name OR if unrecognized returns ``''``.
"""
for property_name, aliases in ordered_property_dict.items():
# Try identical 'key' match first. An exact css_class match must also end with a '-' dash to be valid.
if css_class == property_name or css_class == (property_name + '-'): # No property value included.
return ''
if css_class.startswith(property_name + '-'):
return property_name
# Sort the aliases by descending string length
# This is necessary when the css_class == 'bolder' since 'bold' appears before 'bolder'
aliases = sorted(aliases, key=len, reverse=True)
# Try matching with alias. An alias is not required to end with a dash, but could if it is an abbreviation.
for alias in aliases:
if css_class == alias and alias.endswith('-'): # No property value included.
return ''
if css_class.startswith(alias):
return property_name
# Try matching a regex pattern.
try:
regexes = property_regex_dict[property_name]
for regex in regexes:
if len(findall(regex, css_class)) == 1:
return property_name
except KeyError:
pass
# No match found.
return ''
[docs] @staticmethod
def strip_property_name(property_name='', css_class=''):
"""
Strip property name from css_class if applicable and return the css_class.
:raises ValueError: If either property_name or css_class are empty or only contain whitespace values.
:type property_name: str
:type css_class: str
:param property_name: Presumed to match a key or value in the ``property_alias_dict``.
:param css_class: This value may or may not be identical to the property_value.
:return: (str) -- Returns the encoded property value portion of the css_class.
**Examples:**
>>> ClassPropertyParser.strip_property_name('padding', 'padding-1-2-1-2')
'1-2-1-2'
>>> ClassPropertyParser.strip_property_name('font-weight', 'bold')
'bold'
>>> ClassPropertyParser.strip_property_name('', 'padding-1-2-1-2')
ValueError
>>> ClassPropertyParser.strip_property_name('font-weight', ' ')
ValueError
"""
deny_empty_or_whitespace(string=css_class, variable_name='css_class')
deny_empty_or_whitespace(string=property_name, variable_name='property_name')
property_name += '-' # Append '-' to property to match the class format.
if css_class.startswith(property_name): # Strip property name
return css_class[len(property_name):]
else: # If it doesn't have a property name ignore it.
return css_class
[docs] @staticmethod
def alias_is_abbreviation(possible_alias=''):
"""
Detects if the alias is an abbreviation e.g. ``fw-`` stands for ``font-weight-``.
Abbreviated aliases are found in ``datalibrary.property_alias_dict``.
:type possible_alias: str
:param possible_alias: A value that might be an alias.
:return: (bool) -- True if possible_alias ends with a dash ``-``.
**Examples:**
>>> ClassPropertyParser.alias_is_abbreviation('fw-')
True
>>> ClassPropertyParser.alias_is_abbreviation('bold')
False
"""
return possible_alias.endswith('-')
[docs] def get_property_abbreviations(self, property_name=''):
"""
Returns a list of all property abbreviations appearing in ``property_alias_dict``.
:raises KeyError: If ``property_name`` is not found in ``property_alias_dict``.
:param property_name:
:return: (list) -- A list of all property abbreviations appearing in ``property_alias_dict``.
**Example:**
Assume the following key, value pair occurs in ``property_alias_dict``:
``'background-color': {'bgc-', 'bg-c-', 'bg-color-', },``
>>> property_parser = ClassPropertyParser()
>>> property_parser.get_property_abbreviations('background-color')
['bgc-', 'bg-c-', 'bg-color-']
>>> property_parser.get_property_abbreviations('invalid_property_name')
KeyError
"""
property_abbreviations = list()
for alias in property_alias_dict[property_name]:
if self.alias_is_abbreviation(possible_alias=alias):
property_abbreviations.append(alias)
return property_abbreviations
[docs] def strip_property_abbreviation(self, property_name='', css_class=''):
"""
Strip property abbreviation from css_class if applicable and return the result.
:raises ValueError: If either property_name or css_class are empty or only contain whitespace values.
:type property_name: str
:type css_class: str
:param property_name: Presumed to match a key or value in the ``property_alias_dict``
:param css_class: Initially this value may or may not contain the property_name.
:return: (str) -- Returns the encoded property value portion of the css_class.
**Examples:**
>>> property_parser = ClassPropertyParser()
>>> property_parser.strip_property_abbreviation('color', 'c-lime')
'lime'
>>> property_parser.strip_property_abbreviation('', 'c-lime')
ValueError
>>> property_parser.strip_property_abbreviation('color', ' ')
ValueError
"""
deny_empty_or_whitespace(string=css_class, variable_name='css_class')
deny_empty_or_whitespace(string=property_name, variable_name='property_name')
property_abbreviations = self.get_property_abbreviations(property_name=property_name)
for property_abbreviation in property_abbreviations:
if css_class.startswith(property_abbreviation):
return css_class[len(property_abbreviation):]
return css_class
# Property Value
#
[docs] def get_encoded_property_value(self, property_name='', css_class=''):
"""
Strip property name or alias abbreviation prefix from front, pseudo item, and property priority designator.
Returns the encoded_property_value.
The term encoded_property_value means a property value that represents a css property value,
and may or may not contain dashes and underscores.
:raises ValueError: If either property_name or css_class are empty or only contain whitespace values.
:type property_name: str
:type css_class: str
:param property_name: Name of CSS property that matches a key in ``property_alias_dict``.
:param css_class: An encoded css class selector that may contain property name, value, and priority designator.
:return: (str) -- Returns only the encoded_property_value after the name and priority designator are stripped.
**Examples:**
>>> property_parser = ClassPropertyParser()
>>> property_parser.get_encoded_property_value('font-weight', 'fw-bold-i') # abbreviated property_name
'bold'
>>> property_parser.get_encoded_property_value('padding', 'padding-1-10-10-5-i') # standard property_name
'1-10-10-5'
>>> property_parser.get_encoded_property_value('height', 'height-7_25rem-i') # contains underscores
'7_25rem'
>>> property_parser.get_encoded_property_value('font-style', 'font-style-oblique') # no priority designator
'oblique'
>>> property_parser.get_encoded_property_value('', 'c-lime')
ValueError
>>> property_parser.get_encoded_property_value('color', ' ')
ValueError
"""
css_class = self.strip_property_name(property_name, css_class)
css_class = self.strip_property_abbreviation(property_name, css_class)
css_class = self.strip_pseudo_item(css_class)
encoded_property_value = self.strip_priority_designator(css_class)
return encoded_property_value
[docs] @staticmethod
def get_property_value(property_name='', encoded_property_value=''):
"""
Accepts an encoded_property_value that's been stripped of it's property named and priority
Uses CSSPropertyValueParser, and returns a valid css property value or ''.
Usage Note: Invalid CSS values can be returned by this method. CSS validation occurs at a later step.
:raises ValueError: If either property_name or css_class are empty or only contain whitespace values.
:type property_name: str
:type encoded_property_value: str
:param property_name: Name of CSS property that matches a key in ``property_alias_dict``.
:param encoded_property_value: A property value that may or may not contain dashes and underscores.
:return: (str) -- An unvalidated / potential CSS property value.
**Examples:**
>>> property_parser = ClassPropertyParser(set(), False) # Turn OFF unit conversion.
>>> property_parser.get_property_value('font-weight', 'bold') # Special case: alias == property value
'bold'
>>> property_parser.get_property_value('padding', '1-10-10-5') # Multi-value encoding contains dashes.
'1px 10px 10px 5px'
>>> property_parser.get_property_value('width', '7_25rem') # Single value contains underscores.
'7.25rem'
>>> property_parser.get_property_value('margin', '1ap-10xp-3qp-1mp3') # Invalid CSS returned.
'1a% 10x% 3q% 1mp3'
>>> property_parser.get_property_value('', 'c-lime')
ValueError
>>> property_parser.get_property_value('color', ' ')
ValueError
"""
deny_empty_or_whitespace(string=property_name, variable_name='property_name')
deny_empty_or_whitespace(string=encoded_property_value, variable_name='encoded_property_value')
value_parser = CSSPropertyValueParser(property_name=property_name)
value = value_parser.decode_property_value(value=encoded_property_value)
return value
# Property Priority
#
[docs] def is_important(self, css_class=''):
"""
Tests whether the css_class ends with the importance_designator.
:raises ValueError: If the css_class is empty or only contain whitespace values.
:type css_class: str
:param css_class: An encoded css class selector that may contain property name, value, and priority designator.
:return: (bool) -- Returns True if the css_class ends with the importance_designator. Otherwise, returns False.
**Examples:**
>>> property_parser = ClassPropertyParser()
>>> property_parser.is_important('line-through-i')
True
>>> property_parser.is_important('line-through')
False
>>> property_parser.is_important('')
ValueError
>>> property_parser.is_important(' ')
ValueError
"""
css_class = self.strip_pseudo_item(css_class=css_class)
return css_class.endswith(self.importance_designator)
[docs] def strip_priority_designator(self, css_class=''):
"""
Removes the priority designator, if necessary.
:raises ValueError: If the css_class is empty or only contain whitespace values.
:type css_class: str
:param css_class: A css_class that may or may not contain a priority designator.
:return: (str) -- If necessary, strip priority designator from the end of css_class and return the string.
If the importance_designator is not found, then it returns the unchanged css_class.
>>> property_parser = ClassPropertyParser()
>>> property_parser.strip_priority_designator('blink-i')
'blink'
>>> property_parser.strip_priority_designator('blink')
'blink'
>>> property_parser.strip_priority_designator('')
ValueError
>>> property_parser.strip_priority_designator(' ')
ValueError
"""
if self.is_important(css_class=css_class):
return css_class[:-len(self.importance_designator)]
else:
return css_class
[docs] def get_property_priority(self, css_class=''):
"""
Returns the keyword ``'important'`` or ``''``. These are in the form that ``cssutils`` understands.
:raises ValueError: If the css_class is empty or only contain whitespace values.
:type css_class: str
:param css_class: An encoded css class selector that may contain property name, value, and priority designator.
:return: (str) -- Returns the keyword ``important`` if the property priority is set to important.
Otherwise, it returns ''.
>>> property_parser = ClassPropertyParser()
>>> property_parser.get_property_priority('text-align-center-i')
'important'
>>> property_parser.get_property_priority('text-align-center')
''
>>> property_parser.get_property_priority('')
ValueError
>>> property_parser.get_property_priority(' ')
ValueError
"""
css_class = self.strip_pseudo_item(css_class=css_class)
return 'important' if self.is_important(css_class=css_class) else ''
[docs] def set_pseudo_class(self, css_class=''):
""" Check the pseudo class set for a match. Sets ``pseudo_class`` if found. Otherwise, returns ''.
:raises ValueError: If either property_name or css_class are empty or only contain whitespace values.
:type css_class: str
:param css_class: This value may or may not be identical to the property_value.
:return: None
"""
deny_empty_or_whitespace(string=css_class, variable_name='css_class')
self.pseudo_class = ''
for pseudo_class in pseudo_classes:
if self.is_valid_pseudo_format(pseudo_class, css_class):
self.pseudo_class = pseudo_class
[docs] def set_pseudo_element(self, css_class=''):
""" Check the pseudo element set for a match. Sets the ``pseudo_element`` if found. Otherwise, returns ''.
:raises ValueError: If either property_name or css_class are empty or only contain whitespace values.
:type css_class: str
:param css_class: This value may or may not be identical to the property_value.
:return: None
"""
deny_empty_or_whitespace(string=css_class, variable_name='css_class')
self.pseudo_element = ''
for pseudo_element in pseudo_elements:
if self.is_valid_pseudo_format(pseudo_element, css_class):
self.pseudo_element = pseudo_element
[docs] def strip_pseudo_item(self, css_class=''):
""" Strip property name from css_class if applicable and return the css_class.
*Note:* This method must be called after ``strip_property_name()``.
:raises ValueError: If either pseudo_item or css_class are empty or only contain whitespace values.
:type css_class: str
:param css_class: _value.
:return: (str) -- Returns the encoded property value portion of the css_class.
**Examples:**
>>> ClassPropertyParser.strip_pseudo_item('hover-padding-1-2-1-2')
'1-2-1-2'
>>> ClassPropertyParser.strip_pseudo_item('before-bold')
'bold'
>>> ClassPropertyParser.strip_pseudo_item('after-1-2-1-2')
ValueError
>>> ClassPropertyParser.strip_pseudo_item(' ')
ValueError
"""
deny_empty_or_whitespace(string=css_class, variable_name='css_class')
self.set_pseudo_class(css_class=css_class)
self.set_pseudo_element(css_class=css_class)
if self.pseudo_class: # Prepend '-' to property to match the class format.
pseudo_item = '-' + self.pseudo_class
return css_class.replace(pseudo_item, '')
elif self.pseudo_element:
pseudo_item = '-' + self.pseudo_element
return css_class.replace(pseudo_item, '')
else: # No pseudo encoding found.
return css_class