Edit Book: Live CSS: Fix clicking on link to go to style definition not working if the stylesheet contains CSS 3 media queries

Adds support for CSS 3 media query parsing to tinycss
This commit is contained in:
Kovid Goyal 2014-06-08 18:09:47 +05:30
parent 783b1d112b
commit 1359f475af
6 changed files with 185 additions and 10 deletions

View File

@ -10,7 +10,7 @@ from calibre.gui2.tweak_book.editor.smart import NullSmarts
def find_rule(raw, rule_address):
import tinycss
parser = tinycss.make_parser('page3', 'fonts3')
parser = tinycss.make_full_parser()
sheet = parser.parse_stylesheet(raw)
rules = sheet.rules
ans = None, None

View File

@ -12,14 +12,16 @@
from .version import VERSION
__version__ = VERSION
from .css21 import CSS21Parser
from .page3 import CSSPage3Parser
from .fonts3 import CSSFonts3Parser
from tinycss.css21 import CSS21Parser
from tinycss.page3 import CSSPage3Parser
from tinycss.fonts3 import CSSFonts3Parser
from tinycss.media3 import CSSMedia3Parser
PARSER_MODULES = {
'page3': CSSPage3Parser,
'fonts3': CSSFonts3Parser,
'media3': CSSMedia3Parser,
}
@ -42,3 +44,8 @@ def make_parser(*features, **kwargs):
else:
parser_class = CSS21Parser
return parser_class(**kwargs)
def make_full_parser(**kwargs):
''' A parser that parses all supported CSS 3 modules in addition to CSS 2.1 '''
features = tuple(PARSER_MODULES.iterkeys())
return make_parser(*features, **kwargs)

View File

@ -544,9 +544,7 @@ class CSS21Parser(object):
def parse_media_rule(self, rule, previous_rules, errors, context):
if context != 'stylesheet':
raise ParseError(rule, '@media rule not allowed in ' + context)
if not rule.head:
raise ParseError(rule, 'expected media types for @media')
media = self.parse_media(rule.head)
media = self.parse_media(rule.head, errors)
if rule.body is None:
raise ParseError(rule,
'invalid {0} rule: missing block'.format(rule.at_keyword))
@ -575,7 +573,7 @@ class CSS21Parser(object):
'expected URI or STRING for @import rule, got '
+ head[0].type)
uri = head[0].value
media = self.parse_media(strip_whitespace(head[1:]))
media = self.parse_media(strip_whitespace(head[1:]), errors)
if rule.body is not None:
# The position of the ';' token would be best, but we dont
# have it anymore here.
@ -585,7 +583,7 @@ class CSS21Parser(object):
def parse_charset_rule(self, rule, previous_rules, errors, context):
raise ParseError(rule, 'mis-placed or malformed @charset rule')
def parse_media(self, tokens):
def parse_media(self, tokens, errors):
"""For CSS 2.1, parse a list of media types.
Media Queries are expected to override this.

104
src/tinycss/media3.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
from tinycss.css21 import CSS21Parser
from tinycss.parsing import remove_whitespace, split_on_comma, ParseError
class MediaQuery(object):
__slots__ = 'media_type', 'expressions', 'negated'
def __init__(self, media_type='all', expressions=(), negated=False):
self.media_type = media_type
self.expressions = expressions
self.negated = negated
def __repr__(self):
return '<MediaQuery type=%s negated=%s expressions=%s>' % (
self.media_type, self.negated, self.expressions)
def __eq__(self, other):
return self.media_type == getattr(other, 'media_type', None) and \
self.negated == getattr(other, 'negated', None) and \
self.expressions == getattr(other, 'expressions', None)
class MalformedExpression(Exception):
def __init__(self, tok, msg):
Exception.__init__(self, msg)
self.tok = tok
class CSSMedia3Parser(CSS21Parser):
''' Parse media queries as defined by the CSS 3 media module '''
def parse_media(self, tokens, errors):
if not tokens:
return [MediaQuery('all')]
queries = []
for part in split_on_comma(remove_whitespace(tokens)):
negated = False
media_type = None
expressions = []
try:
for i, tok in enumerate(part):
if i == 0 and tok.type == 'IDENT':
val = tok.value.lower()
if val == 'only':
continue # ignore leading ONLY
if val == 'not':
negated = True
continue
if media_type is None and tok.type == 'IDENT':
media_type = tok.value
continue
elif media_type is None:
media_type = 'all'
if tok.type == 'IDENT' and tok.value.lower() == 'and':
continue
if not tok.is_container:
raise MalformedExpression(tok, 'expected a media expression not a %s' % tok.type)
if tok.type != '(':
raise MalformedExpression(tok, 'media expressions must be in parentheses not %s' % tok.type)
content = remove_whitespace(tok.content)
if len(content) == 0:
raise MalformedExpression(tok, 'media expressions cannot be empty')
if content[0].type != 'IDENT':
raise MalformedExpression(content[0], 'expected a media feature not a %s' % tok.type)
media_feature, expr = content[0].value, None
if len(content) > 1:
if len(content) < 3:
raise MalformedExpression(content[1], 'malformed media feature definition')
if content[1].type != ':':
raise MalformedExpression(content[1], 'expected a :')
expr = content[2:]
if len(expr) == 1:
expr = expr[0]
elif len(expr) == 3 and (expr[0].type, expr[1].type, expr[1].value, expr[2].type) == (
'INTEGER', 'DELIM', '/', 'INTEGER'):
# This should really be moved into token_data, but
# since RATIO is not part of CSS 2.1 and does not
# occur anywhere else, we special case it here.
r = expr[0]
r.value = (expr[0].value, expr[2].value)
r.type = 'RATIO'
r._as_css = expr[0]._as_css + expr[1]._as_css + expr[2]._as_css
expr = r
else:
raise MalformedExpression(expr[0], 'malformed media feature definition')
expressions.append((media_feature, expr))
except MalformedExpression as err:
errors.extend(ParseError(err.tok, err.message))
media_type, negated, expressions = 'all', True, ()
queries.append(MediaQuery(media_type or 'all', expressions=tuple(expressions), negated=negated))
return queries

View File

@ -303,10 +303,10 @@ class TestCSS21(BaseTest):
def test_at_media(self):
for (css_source, expected_rules, expected_errors) in [
(' /* hey */\n', [], []),
('@media {}', [(['all'], [])], []),
('@media all {}', [(['all'], [])], []),
('@media screen, print {}', [(['screen', 'print'], [])], []),
('@media all;', [], ['invalid @media rule: missing block']),
('@media {}', [], ['expected media types for @media']),
('@media 4 {}', [], ['expected a media type, got INTEGER']),
('@media , screen {}', [], ['expected a media type']),
('@media screen, {}', [], ['expected a media type']),

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
from tinycss.media3 import CSSMedia3Parser, MediaQuery as MQ
from tinycss.tests import BaseTest, jsonify
def jsonify_expr(e):
if e is None:
return None
return next(jsonify([e]))
def jsonify_expressions(mqlist):
for mq in mqlist:
mq.expressions = tuple(
(k, jsonify_expr(e)) for k, e in mq.expressions)
return mqlist
class TestFonts3(BaseTest):
def test_media_queries(self):
'Test parsing of media queries from the CSS 3 media module'
for css, media_query_list, expected_errors in [
# CSS 2.1 (simple media queries)
('@media {}', [MQ()], []),
('@media all {}', [MQ()], []),
('@media screen {}', [MQ('screen')], []),
('@media , screen {}', [MQ(), MQ('screen')], []),
('@media screen, {}', [MQ('screen'), MQ()], []),
# Examples from the CSS 3 specs
('@media screen and (color) {}', [MQ('screen', (('color', None),))], []),
('@media all and (min-width:500px) {}', [
MQ('all', (('min-width', ('DIMENSION', 500)),))], []),
('@media (min-width:500px) {}', [
MQ('all', (('min-width', ('DIMENSION', 500)),))], []),
('@media (orientation: portrait) {}', [
MQ('all', (('orientation', ('IDENT', 'portrait')),))], []),
('@media screen and (color), projection and (color) {}', [
MQ('screen', (('color', None),)), MQ('projection', (('color', None),)),], []),
('@media not screen and (color) {}', [
MQ('screen', (('color', None),), True)], []),
('@media only screen and (color) {}', [
MQ('screen', (('color', None),))], []),
('@media aural and (device-aspect-ratio: 16/9) {}', [
MQ('aural', (('device-aspect-ratio', ('RATIO', (16, 9))),))], []),
('@media (resolution: 166dpi) {}', [
MQ('all', (('resolution', ('DIMENSION', 166)),))], []),
('@media (min-resolution: 166DPCM) {}', [
MQ('all', (('min-resolution', ('DIMENSION', 166)),))], []),
# Malformed media queries
('@media (example, all,), speech {}', [MQ(negated=True), MQ('speech')], ['expected a :']),
('@media &test, screen {}', [MQ(negated=True), MQ('screen')], ['expected a media expression not a DELIM']),
]:
stylesheet = CSSMedia3Parser().parse_stylesheet(css)
self.assert_errors(stylesheet.errors, expected_errors)
self.ae(len(stylesheet.rules), 1)
rule = stylesheet.rules[0]
self.ae(jsonify_expressions(rule.media), media_query_list)