Skip to content

Instantly share code, notes, and snippets.

@mitchellrj
Created January 17, 2014 18:06
Show Gist options
  • Save mitchellrj/8478402 to your computer and use it in GitHub Desktop.
Save mitchellrj/8478402 to your computer and use it in GitHub Desktop.
Basic CSS3 media query parsing for Python & testing against a dictionary of characteristics
import fractions
import re
def parse_media_query(media_query):
class Grouping(object):
def __init__(self, parent, *args):
self.parent = parent
self.children = list(args)
def add(self, arg):
self.children.add(arg)
def matches(self, test_medium):
return self.children[0].matches(test_medium)
def __str__(self):
if not self.children:
return ''
return '({child})'.format(child=self.children[0])
class MediaExpression(object):
pass
class MediaType(MediaExpression):
def __init__(self, parent, value):
self.parent = parent
self.value = value
def matches(self, test_medium):
return self.value == 'all' or self.value == test_medium['type']
def __str__(self):
return self.value
class MediaFeature(MediaExpression):
def __init__(self, parent, name, value=None):
self.parent = parent
self.name = name
self.set_value(value)
def set_value(self, value):
if value is None:
self.value = None
self.units = None
else:
self.value = self.convert(value)
self.units = self.get_units(value)
def get_units(self, value):
return re.sub(r'[0-9]', '', value.lower().strip())
def convert(self, value):
return int(re.sub(r'[^0-9]', '', value.lower().strip()))
def matches(self, test_medium):
if self.value is None:
return bool(test_medium[self.name])
return self.value == self.convert(test_medium[self.name])
def __str__(self):
if self.value is None:
return self.name
return '{name}: {value}{units}'.format(
name=self.name,
value=self.value,
units=(self.units or '')
)
class ScanMediaFeature(MediaFeature):
def get_units(self, value):
return None
def convert(self, value):
return value
class MinMaxMediaFeature(MediaFeature):
def matches(self, test_medium):
if self.name.startswith('min-'):
return self <= self.convert(test_medium[self.name[4:]])
elif self.name.startswith('max-'):
return self >= self.convert(test_medium[self.name[4:]])
else:
return super(MediaFeature, self).matches(test_medium)
def __ge__(self, test_value):
return self.value >= test_value
def __le__(self, test_value):
return self.value <= test_value
def __eq__(self, test_value):
return self.value == test_value
class AspectRatioMediaFeature(MinMaxMediaFeature):
def convert(self, value):
width, height = value.strip().split('/', 1)
return int(width) / int(height)
def get_units(self, value):
return None
def __str__(self):
fraction = fractions.Fraction(self.value)
return '{name}: {value}'.format(
name=self.name,
value=fraction,
)
class ResolutionMediaFeature(MinMaxMediaFeature):
CM_IN_INCH = 2.54
def convert(self, value):
int_value = super(MinMaxMediaFeature, self).convert(value)
if self.get_units(value) == 'dpcm':
int_value = int_value * self.CM_IN_INCH
return int_value
class Whitespace(object):
def __init__(self, whitespace):
pass
class Value(object):
def __init__(self, value):
pass
class And(Grouping):
def matches(self, test_medium):
for child in self.children:
if not child.matches(test_medium):
return False
return True
def __str__(self):
return ' and '.join(map(str, self.children))
class Or(Grouping):
def matches(self, test_medium):
for child in self.children:
if child.matches(test_medium):
return True
return False
def __str__(self):
return ', '.join(map(str, self.children))
class Not(Grouping):
def matches(self, test_medium):
return not self.children[0].matches(test_medium)
def __str__(self):
return 'not {query}'.format(query=self.children[0])
class Assignment(object):
def __init__(self, assignment):
pass
tokens = {
r'only': Whitespace,
r'not': Not,
r',': Or,
r'and': And,
r'\(': Grouping,
r'\)': Grouping,
r' ': Whitespace,
r'\t': Whitespace,
r'\n': Whitespace,
r':': Assignment,
r'all': MediaType,
r'aural': MediaType,
r'braille': MediaType,
r'handheld': MediaType,
r'print': MediaType,
r'projection': MediaType,
r'screen': MediaType,
r'tty': MediaType,
r'tv': MediaType,
r'embossed': MediaType,
r'width': MinMaxMediaFeature,
r'min-width': MinMaxMediaFeature,
r'max-width': MinMaxMediaFeature,
r'height': MinMaxMediaFeature,
r'min-height': MinMaxMediaFeature,
r'max-height': MinMaxMediaFeature,
r'device-width': MinMaxMediaFeature,
r'min-device-width': MinMaxMediaFeature,
r'max-device-width': MinMaxMediaFeature,
r'device-height': MinMaxMediaFeature,
r'min-device-height': MinMaxMediaFeature,
r'max-device-height': MinMaxMediaFeature,
r'aspect-ratio': AspectRatioMediaFeature,
r'min-aspect-ratio': AspectRatioMediaFeature,
r'max-aspect-ratio': AspectRatioMediaFeature,
r'device-aspect-ratio': AspectRatioMediaFeature,
r'min-device-aspect-ratio': AspectRatioMediaFeature,
r'max-device-aspect-ratio': AspectRatioMediaFeature,
r'color': MinMaxMediaFeature,
r'min-color': MinMaxMediaFeature,
r'max-color': MinMaxMediaFeature,
r'color-index': MinMaxMediaFeature,
r'min-color-index': MinMaxMediaFeature,
r'max-color': MinMaxMediaFeature,
r'monochrome': MinMaxMediaFeature,
r'min-monochrome': MinMaxMediaFeature,
r'max-monochrome': MinMaxMediaFeature,
r'resolution': ResolutionMediaFeature,
r'min-resolution': ResolutionMediaFeature,
r'max-resolution': ResolutionMediaFeature,
r'scan': ScanMediaFeature,
r'grid': MediaFeature,
}
token_string = '({or_})'.format(or_='|'.join(tokens.keys()))
root = current = Grouping(None)
expect_value = False
for token in re.split(token_string, media_query, flags=re.I):
token = token.lower()
if not token:
continue
if token in tokens:
cls = tokens[token]
elif token in '()':
cls = Grouping
else:
cls = Value
if expect_value and cls not in (Whitespace, Value):
raise ValueError(u'Expected value.')
if cls == Whitespace:
pass
elif cls == Grouping:
if token == ')':
current = current.parent
else:
current = Grouping(parent=current)
current.parent.children.append(current)
elif cls in (And, Or):
current.parent.children.remove(current)
current = cls(current.parent, current)
current.parent.children.append(current)
elif issubclass(cls, MediaExpression):
current = cls(current, token)
current.parent.children.append(current)
if (
isinstance(current.parent, Grouping)
and current.parent.__class__ != Grouping
):
# Close AND / OR / NOT groupings.
current = current.parent.parent
elif cls == Assignment:
assert isinstance(current, MediaFeature)
expect_value = True
elif cls == Value:
if not expect_value:
raise ValueError(u'Unexpected %r' % (token,))
current.set_value(token)
expect_value = False
return root
def media_query_matches(media_query, display_medium):
query = parse_media_query(media_query)
return query.matches(display_medium)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment