Skip to content

Commit

Permalink
Proper media queries
Browse files Browse the repository at this point in the history
The lexer now identifies media queries in @media ... {} and
@import ... ; statements. For that, two new states "mediaquery" and
import" where added. The parser uses those to implement the CSS3 media
query BNF (www.w3.org/TR/css3-mediaqueries). Overall test coverage
increased.
  • Loading branch information
saschpe committed Jan 21, 2014
1 parent 1c2240d commit bba63d9
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 34 deletions.
70 changes: 70 additions & 0 deletions lesscpy/lessc/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class LessLexer:
('istringquotes', 'inclusive'),
('istringapostrophe', 'inclusive'),
('iselector', 'inclusive'),
('mediaquery', 'inclusive'),
('import', 'inclusive'),
)
literals = '<>=%!/*-+&'
tokens = [
Expand All @@ -44,6 +46,13 @@ class LessLexer:
'css_uri',
'css_ms_filter',

'css_media_type',
'css_media_feature',

't_and',
't_not',
't_only',

'less_variable',
'less_comment',
'less_open_format',
Expand Down Expand Up @@ -79,7 +88,11 @@ class LessLexer:
'css_ident',
'css_number',
'css_color',
'css_media_type',
'less_variable',
't_and',
't_not',
't_only',
'&',
])
significant_ws.update(reserved.tokens.values())
Expand Down Expand Up @@ -219,12 +232,69 @@ def t_iselector_t_colon(self, t):
t.lexer.pop_state()
return t

def t_mediaquery_t_not(self, t):
r'not'
return t

def t_mediaquery_t_only(self, t):
r'only'
return t

def t_mediaquery_t_and(self, t):
r'and'
return t

def t_mediaquery_t_popen(self, t):
r'\('
# Redefine global t_popen to avoid pushing state 'parn'
return t

@lex.TOKEN('|'.join(css.media_types))
def t_mediaquery_css_media_type(self, t):
return t

@lex.TOKEN('|'.join(css.media_features))
def t_mediaquery_css_media_feature(self, t):
return t

def t_mediaquery_t_bopen(self, t):
r'\{'
t.lexer.pop_state()
return t

def t_mediaquery_t_semicolon(self, t):
r';'
# This can happen only as part of a CSS import statement. The
# "mediaquery" state is reused there. Ordinary media queries always
# end at '{', i.e. when a block is opened.
t.lexer.pop_state() # state mediaquery
# We have to pop the 'import' state here because we already ate the
# t_semicolon and won't trigger t_import_t_semicolon.
t.lexer.pop_state() # state import
return t

@lex.TOKEN('|'.join(css.media_types))
def t_import_css_media_type(self, t):
# Example: @import url("bar.css") handheld and (max-width: 500px);
# Alternatively, we could use a lookahead "if not ';'" after the URL
# part of the @import statement...
t.lexer.push_state("mediaquery")
return t

def t_import_t_semicolon(self, t):
r';'
t.lexer.pop_state()
return t

def t_less_variable(self, t):
r'@@?[\w-]+|@\{[^@\}]+\}'
v = t.value.lower()
if v in reserved.tokens:
t.type = reserved.tokens[v]
if t.type == "css_media":
t.lexer.push_state("mediaquery")
elif t.type == "css_import":
t.lexer.push_state("import")
return t

def t_css_color(self, t):
Expand Down
114 changes: 104 additions & 10 deletions lesscpy/lessc/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,20 @@ def p_statement_namespace(self, p):

def p_statement_import(self, p):
""" import_statement : css_import t_ws css_string t_semicolon
| css_import t_ws css_string dom t_semicolon
| css_import t_ws css_string media_query_list t_semicolon
| css_import t_ws fcall t_semicolon
| css_import t_ws fcall media_query_list t_semicolon
"""
if self.importlvl > 8:
raise ImportError(
'Recrusive import level too deep > 8 (circular import ?)')
ipath = utility.destring(p[3])
if isinstance(p[3], str):
ipath = utility.destring(p[3])
elif isinstance(p[3], Call):
# NOTE(saschpe): Always in the form of 'url("...");', so parse it
# and retrieve the inner css_string. This whole func is messy.
p[3] = p[3].parse(self.scope) # Store it as string, Statement.fmt expects it.
ipath = utility.destring(p[3][4:-1])
fn, fe = os.path.splitext(ipath)
if not fe or fe.lower() == '.less':
try:
Expand Down Expand Up @@ -223,6 +231,11 @@ def p_block_open(self, p):
p[0] = p[1]
self.scope.current = p[1]

def p_block_open_media_query(self, p):
""" block_open : media_query_decl brace_open
"""
p[0] = Identifier(p[1]).parse(self.scope)

def p_font_face_open(self, p):
""" block_open : css_font_face t_ws brace_open
"""
Expand Down Expand Up @@ -510,20 +523,77 @@ def p_ident_parts(self, p):
p[1] = [p[1]]
p[0] = p[1]

def p_ident_media(self, p):
""" ident_parts : css_media t_ws
| css_media t_ws t_popen word t_colon number t_pclose
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#

def p_media_query_decl(self, p):
""" media_query_decl : css_media t_ws
| css_media t_ws media_query_list
"""
p[0] = list(p)[1:]

def p_media_query_list_aux(self, p):
""" media_query_list : media_query_list t_comma media_query
"""
p[0] = list(p)[1:]

def p_media_query_list(self, p):
""" media_query_list : media_query
"""
p[0] = [p[1]]

def p_media_query_a(self, p):
""" media_query : media_type
| media_type media_query_expression_list
| not media_type
| not media_type media_query_expression_list
| only media_type
| only media_type media_query_expression_list
"""
p[0] = list(p)[1:]

def p_media_query_b(self, p):
""" media_query : media_query_expression media_query_expression_list
| media_query_expression
"""
p[0] = list(p)[1:]

def p_media_query_expression_list_aux(self, p):
""" media_query_expression_list : media_query_expression_list and media_query_expression
| and media_query_expression
"""
p[0] = list(p)[1:]

def p_ident_media_var(self, p):
""" ident_parts : css_media t_ws t_popen word t_colon variable t_pclose
def p_media_query_expression(self, p):
""" media_query_expression : t_popen css_media_feature t_pclose
| t_popen css_media_feature t_colon media_query_value t_pclose
"""
p[0] = list(p)[1:]
if utility.is_variable(p[0][5]):
var = self.scope.variables(''.join(p[0][5]))

def p_media_query_value(self, p):
""" media_query_value : number
| variable
| word
| color
| expression
"""
if utility.is_variable(p[1]):
var = self.scope.variables(''.join(p[1]))
if var:
p[0][5] = var.value[0]
value = var.value[0]
if hasattr(value, 'parse'):
p[1] = value.parse(self.scope)
else:
p[1] = value
if isinstance(p[1], Expression):
p[0] = p[1].parse(self.scope)
else:
p[0] = p[1]

#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#

def p_selector(self, p):
""" selector : '*'
Expand Down Expand Up @@ -818,6 +888,12 @@ def p_vendor_property(self, p):
"""
p[0] = tuple(list(p)[1:])

def p_media_type(self, p):
""" media_type : css_media_type
| css_media_type t_ws
"""
p[0] = tuple(list(p)[1:])

def p_combinator(self, p):
""" combinator : '&' t_ws
| '&'
Expand Down Expand Up @@ -847,6 +923,24 @@ def p_scope_close(self, p):
"""
p[0] = p[1]

def p_and(self, p):
""" and : t_and t_ws
| t_and
"""
p[0] = tuple(list(p)[1:])

def p_not(self, p):
""" not : t_not t_ws
| t_not
"""
p[0] = tuple(list(p)[1:])

def p_only(self, p):
""" only : t_only t_ws
| t_only
"""
p[0] = tuple(list(p)[1:])

def p_empty(self, p):
'empty :'
pass
Expand Down
64 changes: 64 additions & 0 deletions lesscpy/lib/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,67 @@
'zoom',
]
propertys = css2 + css3 + svg + vendor_ugly

# CSS-2(.1) media types: http://www.w3.org/TR/CSS2/media.html#media-types
# Include media types as defined in HTML4: http://www.w3.org/TR/1999/REC-html401-19991224/types.html#h-6.13
# Also explained in http://www.w3.org/TR/css3-mediaqueries/#background
html4_media_types = [
'all',
'aural', # deprecated by CSS 2.1, which reserves "speech"
'braille',
'handheld',
'print',
'projection',
'screen',
'tty',
'tv',
]
css2_media_types = [
'embossed', # CSS2, not HTML4
'speech', # CSS2. not HTML4
]
media_types = html4_media_types + css2_media_types

css3_media_features = [
'width',
'min-width',
'max-width',
'height',
'min-height',
'max-height',
'device-width',
'min-device-width',
'max-device-width',
'device-height',
'min-device-height',
'max-device-height',
'orientation',
'aspect-ratio',
'min-aspect-ratio',
'max-aspect-ratio',
'device-aspect-ratio',

'min-device-aspect-ratio',
'max-device-aspect-ratio',
'color',
'min-color',
'max-color',
'color-index',
'min-color-index',
'max-color-index',
'monochrome',
'min-monochrome',
'max-monochrome',
'resolution',
'min-resolution',
'max-resolution',
'scan',
'grid',
]
vendor_media_features = [
'-webkit-min-device-pixel-ratio',
'min--moz-device-pixel-ratio',
'-o-min-device-pixel-ratio',
'min-device-pixel-ratio',
]
media_features = css3_media_features + vendor_media_features
23 changes: 1 addition & 22 deletions lesscpy/lib/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,28 +158,8 @@
'tspan',
]

# CSS-2(.1) media types: http://www.w3.org/TR/CSS2/media.html#media-types
# Include media types as defined in HTML4: http://www.w3.org/TR/1999/REC-html401-19991224/types.html#h-6.13
# Also explained in http://www.w3.org/TR/css3-mediaqueries/#background
html4_media_types = [
'all',
'aural', # deprecated by CSS 2.1, which reserves "speech"
'braille',
'handheld',
'print',
'projection',
'screen',
'tty',
'tv',
]
css2_media_types = [
'embossed', # CSS2, not HTML4
'speech', # CSS2. not HTML4
]
media_types = html4_media_types + css2_media_types

# Check http://www.w3.org/TR/css3-animations/#keyframes
# Treating them as DOM elements isn't entirely accurate (same for media types)
# Treating them as DOM elements isn't entirely accurate
# but sufficent for our purposes.
css3_animation_keyframe_selectors = [
'from',
Expand All @@ -189,5 +169,4 @@
elements = html4
elements.extend(html5)
elements.extend(svg)
elements.extend(media_types)
elements.extend(css3_animation_keyframe_selectors)
5 changes: 5 additions & 0 deletions lesscpy/test/css/imports.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@
width: 6px;
height: 9px;
}
@import "styles.css";
@import url("styles.css");
@import url("druck.css") print;
@import url("foo.css") projection,tv;
@import url("bar.css") handheld and (max-width:500px);
5 changes: 5 additions & 0 deletions lesscpy/test/css/imports.min.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@
.mixin{color:red;}
.mixin{color:red;}
.import{color:red;width:6px;height:9px;}
@import "styles.css";
@import url("styles.css");
@import url("druck.css") print;
@import url("foo.css") projection,tv;
@import url("bar.css") handheld and (max-width:500px);
Loading

0 comments on commit bba63d9

Please sign in to comment.