mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
783b1d112b
commit
1359f475af
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 don’t
|
||||
# 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
104
src/tinycss/media3.py
Normal 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
|
||||
|
@ -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']),
|
||||
|
66
src/tinycss/tests/media3.py
Normal file
66
src/tinycss/tests/media3.py
Normal 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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user