diff options
author | Eevee (Alex Munroe) <eevee.git@veekun.com> | 2014-08-29 19:48:37 -0700 |
---|---|---|
committer | Eevee (Alex Munroe) <eevee.git@veekun.com> | 2014-08-29 19:48:37 -0700 |
commit | e4eb845498108f69e0b6f72eacb6e94aed6b120f (patch) | |
tree | 3a01688995bdd558d9d53a70640c3e6c1368b0e3 | |
parent | a42962221824dfe7af5067ca05da8a2858fc0dbf (diff) | |
download | pyscss-e4eb845498108f69e0b6f72eacb6e94aed6b120f.tar.gz |
Fix most of the catastrophic fallout.
- Unquoted strings really shouldn't be re-escaped at all; oops.
- Use the Url type a few places instead of manually escaping.
- Make a Function type.
- Fix the parsing of maps, and left-factor to get rid of the KW* tokens.
-rw-r--r-- | scss/extension/compass/helpers.py | 30 | ||||
-rw-r--r-- | scss/extension/compass/images.py | 9 | ||||
-rw-r--r-- | scss/extension/fonts.py | 16 | ||||
-rw-r--r-- | scss/grammar/expression.g | 47 | ||||
-rw-r--r-- | scss/grammar/expression.py | 111 | ||||
-rw-r--r-- | scss/types.py | 61 |
6 files changed, 147 insertions, 127 deletions
diff --git a/scss/extension/compass/helpers.py b/scss/extension/compass/helpers.py index 85c67dd..b218e00 100644 --- a/scss/extension/compass/helpers.py +++ b/scss/extension/compass/helpers.py @@ -15,8 +15,8 @@ import six from scss import config from scss.namespace import Namespace -from scss.types import Boolean, List, Null, Number, String -from scss.util import escape, to_str, getmtime, make_data_url +from scss.types import Boolean, Function, List, Null, Number, String, Url +from scss.util import to_str, getmtime, make_data_url import re log = logging.getLogger(__name__) @@ -541,7 +541,9 @@ def _font_url(path, only_path=False, cache_buster=True, inline=False): if re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value): font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value).groups()[1]).value - if not FONT_TYPES.get(font_type): + try: + mime = FONT_TYPES[font_type] + except KeyError: raise Exception('Could not determine font type for "%s"' % path.value) mime = FONT_TYPES.get(font_type) @@ -558,9 +560,10 @@ def _font_url(path, only_path=False, cache_buster=True, inline=False): if cache_buster and filetime != 'NA': url = add_cache_buster(url, filetime) - if not only_path: - url = 'url(%s)' % escape(url) - return String.unquoted(url) + if only_path: + return String.unquoted(url) + else: + return Url.unquoted(url) def _font_files(args, inline): @@ -570,8 +573,7 @@ def _font_files(args, inline): fonts = [] args_len = len(args) skip_next = False - for index in range(len(args)): - arg = args[index] + for index, arg in enumerate(args): if not skip_next: font_type = args[index + 1] if args_len > (index + 1) else None if font_type and font_type.value in FONT_TYPES: @@ -581,7 +583,10 @@ def _font_files(args, inline): font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value).groups()[1]) if font_type.value in FONT_TYPES: - fonts.append(String.unquoted('%s format("%s")' % (_font_url(arg, inline=inline), String.unquoted(FONT_TYPES[font_type.value]).value))) + fonts.append(List([ + _font_url(arg, inline=inline), + Function(FONT_TYPES[font_type.value], 'format'), + ], use_comma=False)) else: raise Exception('Could not determine font type for "%s"' % arg.value) else: @@ -640,6 +645,7 @@ def stylesheet_url(path, only_path=False, cache_buster=True): url = '%s%s' % (BASE_URL, filepath) if cache_buster: url = add_cache_buster(url, filetime) - if not only_path: - url = 'url("%s")' % (url) - return String.unquoted(url) + if only_path: + return String.unquoted(url) + else: + return Url.unquoted(url) diff --git a/scss/extension/compass/images.py b/scss/extension/compass/images.py index 1374c29..e5c060f 100644 --- a/scss/extension/compass/images.py +++ b/scss/extension/compass/images.py @@ -14,7 +14,7 @@ from . import _image_size_cache from .helpers import add_cache_buster from scss import config from scss.namespace import Namespace -from scss.types import Color, List, Number, String +from scss.types import Color, List, Number, String, Url from scss.util import escape, getmtime, make_data_url, make_filename_hash try: @@ -180,9 +180,10 @@ def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_col if not os.sep == '/': url = url.replace(os.sep, '/') - if not only_path: - url = 'url(%s)' % escape(url) - return String.unquoted(url) + if only_path: + return String.unquoted(url) + else: + return Url.unquoted(url) @ns.declare diff --git a/scss/extension/fonts.py b/scss/extension/fonts.py index 9d2d1e1..e57b3c7 100644 --- a/scss/extension/fonts.py +++ b/scss/extension/fonts.py @@ -27,8 +27,8 @@ except: from scss import config from scss.extension import Extension from scss.namespace import Namespace -from scss.types import String, Boolean, List -from scss.util import getmtime, escape, make_data_url, make_filename_hash +from scss.types import Boolean, List, String, Url +from scss.util import getmtime, make_data_url, make_filename_hash log = logging.getLogger(__name__) @@ -343,11 +343,10 @@ def font_sheet(g, **kwargs): assets = {} for type_, url in urls.items(): format_ = FONT_FORMATS[type_] - url = "url('%s')" % escape(url) if inline: - assets[type_] = inline_assets[type_] = List([String.unquoted(url), String.unquoted(format_)]) + assets[type_] = inline_assets[type_] = List([Url.unquoted(url), String.unquoted(format_)]) else: - assets[type_] = file_assets[type_] = List([String.unquoted(url), String.unquoted(format_)]) + assets[type_] = file_assets[type_] = List([Url.unquoted(url), String.unquoted(format_)]) asset = List([assets[type_] for type_ in FONT_TYPES if type_ in assets], separator=",") # Add the new object: @@ -407,9 +406,10 @@ def font_url(sheet, type_, only_path=False, cache_buster=True): params.append('#' + font_sheet['*n*']) if params: url += '?' + '&'.join(params) - if not only_path: - url = "url('%s')" % escape(url) - return String.unquoted(url) + if only_path: + return String.unquoted(url) + else: + return Url.unquoted(url) return String.unquoted('') diff --git a/scss/grammar/expression.g b/scss/grammar/expression.g index ba5dc86..a881279 100644 --- a/scss/grammar/expression.g +++ b/scss/grammar/expression.g @@ -62,20 +62,15 @@ parser SassExpression: # Don't allow quotes or # unless they're escaped (or the # is alone) token SINGLE_STRING_GUTS: '([^\'\\\\#]|[\\\\].|#(?![{]))*' token DOUBLE_STRING_GUTS: "([^\"\\\\#]|[\\\\].|#(?![{]))*" - token KWSTR: "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'(?=\s*:)" token STR: "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'" - token KWQSTR: '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"(?=\s*:)' token QSTR: '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"' token UNITS: "(?<!\s)(?:[a-zA-Z]+|%)(?![-\w])" - token KWNUM: "(?:\d+(?:\.\d*)?|\.\d+)(?=\s*:)" token NUM: "(?:\d+(?:\.\d*)?|\.\d+)" - token KWCOLOR: "#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])(?=\s*:)" token COLOR: "#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])" token KWVAR: "\$[-a-zA-Z0-9_]+(?=\s*:)" token SLURPYVAR: "\$[-a-zA-Z0-9_]+(?=[.][.][.])" token VAR: "\$[-a-zA-Z0-9_]+" token FNCT: "[-a-zA-Z_][-a-zA-Z0-9_]*(?=\()" - token KWID: "[-a-zA-Z_][-a-zA-Z0-9_]*(?=\s*:)" # TODO Ruby is a bit more flexible here, for example allowing 1#{2}px token BAREWORD: "[-a-zA-Z_][-a-zA-Z0-9_]*" token BANG_IMPORTANT: "!important" @@ -121,20 +116,31 @@ parser SassExpression: | expr_slst {{ return (None, expr_slst) }} - # Maps: - rule expr_map: - map_item {{ pairs = [map_item] }} - ( - "," {{ map_item = (None, None) }} - [ map_item ] {{ pairs.append(map_item) }} - )* {{ return MapLiteral(pairs) }} + # Maps, which necessarily overlap with lists because LL(1): + rule expr_map_or_list: + expr_slst {{ first = expr_slst }} + ( # Colon means this is a map + ":" + expr_slst {{ pairs = [(first, expr_slst)] }} + ( + "," {{ map_item = None, None }} + [ map_item ] {{ pairs.append(map_item) }} + )* {{ return MapLiteral(pairs) }} + | # Comma means this is a comma-delimited list + {{ items = [first]; use_list = False }} + ( + "," {{ use_list = True }} + expr_slst {{ items.append(expr_slst) }} + )* {{ return ListLiteral(items) if use_list else items[0] }} + ) rule map_item: - kwatom ":" expr_slst {{ return (kwatom, expr_slst) }} + atom ":" expr_slst {{ return (atom, expr_slst) }} # Lists: rule expr_lst: + # TODO a trailing comma makes a list now, i believe expr_slst {{ v = [expr_slst] }} ( "," @@ -198,8 +204,7 @@ parser SassExpression: rule atom: LPAR ( {{ v = ListLiteral([], comma=False) }} - | expr_map {{ v = expr_map }} - | expr_lst {{ v = expr_lst }} + | expr_map_or_list {{ v = expr_map_or_list }} ) RPAR {{ return Parentheses(v) }} # Special functions. Note that these technically overlap with the # regular function rule, which makes this not quite LL -- but they're @@ -216,18 +221,6 @@ parser SassExpression: | COLOR {{ return Literal(Color.from_hex(COLOR, literal=True)) }} | VAR {{ return Variable(VAR) }} - # TODO none of these things respect interpolation -- would love to not need - # to repeat all the rules - rule kwatom: - # nothing - | KWID {{ return Literal.from_bareword(KWID) }} - | KWNUM {{ UNITS = None }} - [ UNITS ] {{ return Literal(Number(float(KWNUM), unit=UNITS)) }} - | KWSTR {{ return Literal(String(dequote(KWSTR), quotes="'")) }} - | KWQSTR {{ return Literal(String(dequote(KWQSTR), quotes='"')) }} - | KWCOLOR {{ return Literal(Color.from_hex(KWCOLOR, literal=True)) }} - | KWVAR {{ return Variable(KWVAR) }} - # ------------------------------------------------------------------------- # Interpolation, which is a right mess, because it depends very heavily on # context -- what other characters are allowed, and when do we stop? diff --git a/scss/grammar/expression.py b/scss/grammar/expression.py index a1b37a4..03313f6 100644 --- a/scss/grammar/expression.py +++ b/scss/grammar/expression.py @@ -66,20 +66,15 @@ class SassExpressionScanner(Scanner): ('DOUBLE_QUOTE', '"'), ('SINGLE_STRING_GUTS', "([^'\\\\#]|[\\\\].|#(?![{]))*"), ('DOUBLE_STRING_GUTS', '([^"\\\\#]|[\\\\].|#(?![{]))*'), - ('KWSTR', "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'(?=\\s*:)"), ('STR', "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'"), - ('KWQSTR', '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"(?=\\s*:)'), ('QSTR', '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"'), ('UNITS', '(?<!\\s)(?:[a-zA-Z]+|%)(?![-\\w])'), - ('KWNUM', '(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?=\\s*:)'), ('NUM', '(?:\\d+(?:\\.\\d*)?|\\.\\d+)'), - ('KWCOLOR', '#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])(?=\\s*:)'), ('COLOR', '#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])'), ('KWVAR', '\\$[-a-zA-Z0-9_]+(?=\\s*:)'), ('SLURPYVAR', '\\$[-a-zA-Z0-9_]+(?=[.][.][.])'), ('VAR', '\\$[-a-zA-Z0-9_]+'), ('FNCT', '[-a-zA-Z_][-a-zA-Z0-9_]*(?=\\()'), - ('KWID', '[-a-zA-Z_][-a-zA-Z0-9_]*(?=\\s*:)'), ('BAREWORD', '[-a-zA-Z_][-a-zA-Z0-9_]*'), ('BANG_IMPORTANT', '!important'), ('INTERP_START', '#[{]'), @@ -157,22 +152,35 @@ class SassExpression(Parser): expr_slst = self.expr_slst() return (None, expr_slst) - def expr_map(self): - map_item = self.map_item() - pairs = [map_item] - while self._peek(self.expr_map_rsts) == '","': - self._scan('","') - map_item = (None, None) - if self._peek(self.expr_map_rsts_) not in self.expr_map_rsts: - map_item = self.map_item() - pairs.append(map_item) - return MapLiteral(pairs) + def expr_map_or_list(self): + expr_slst = self.expr_slst() + first = expr_slst + _token_ = self._peek(self.expr_map_or_list_rsts) + if _token_ == '":"': + self._scan('":"') + expr_slst = self.expr_slst() + pairs = [(first, expr_slst)] + while self._peek(self.expr_map_or_list_rsts_) == '","': + self._scan('","') + map_item = None, None + if self._peek(self.expr_map_or_list_rsts__) not in self.expr_map_or_list_rsts_: + map_item = self.map_item() + pairs.append(map_item) + return MapLiteral(pairs) + else: # in self.expr_map_or_list_rsts_ + items = [first]; use_list = False + while self._peek(self.expr_map_or_list_rsts_) == '","': + self._scan('","') + use_list = True + expr_slst = self.expr_slst() + items.append(expr_slst) + return ListLiteral(items) if use_list else items[0] def map_item(self): - kwatom = self.kwatom() + atom = self.atom() self._scan('":"') expr_slst = self.expr_slst() - return (kwatom, expr_slst) + return (atom, expr_slst) def expr_lst(self): expr_slst = self.expr_slst() @@ -186,7 +194,7 @@ class SassExpression(Parser): def expr_slst(self): or_expr = self.or_expr() v = [or_expr] - while self._peek(self.expr_slst_rsts) not in self.expr_lst_rsts: + while self._peek(self.expr_slst_rsts) not in self.expr_slst_chks: or_expr = self.or_expr() v.append(or_expr) return ListLiteral(v, comma=False) if len(v) > 1 else v[0] @@ -301,12 +309,9 @@ class SassExpression(Parser): _token_ = self._peek(self.atom_rsts) if _token_ == 'RPAR': v = ListLiteral([], comma=False) - elif _token_ not in self.argspec_item_chks: - expr_map = self.expr_map() - v = expr_map else: # in self.argspec_item_chks - expr_lst = self.expr_lst() - v = expr_lst + expr_map_or_list = self.expr_map_or_list() + v = expr_map_or_list RPAR = self._scan('RPAR') return Parentheses(v) elif _token_ == '"url"': @@ -343,32 +348,6 @@ class SassExpression(Parser): VAR = self._scan('VAR') return Variable(VAR) - def kwatom(self): - _token_ = self._peek(self.kwatom_rsts) - if _token_ == '":"': - pass - elif _token_ == 'KWID': - KWID = self._scan('KWID') - return Literal.from_bareword(KWID) - elif _token_ == 'KWNUM': - KWNUM = self._scan('KWNUM') - UNITS = None - if self._peek(self.kwatom_rsts_) == 'UNITS': - UNITS = self._scan('UNITS') - return Literal(Number(float(KWNUM), unit=UNITS)) - elif _token_ == 'KWSTR': - KWSTR = self._scan('KWSTR') - return Literal(String(dequote(KWSTR), quotes="'")) - elif _token_ == 'KWQSTR': - KWQSTR = self._scan('KWQSTR') - return Literal(String(dequote(KWQSTR), quotes='"')) - elif _token_ == 'KWCOLOR': - KWCOLOR = self._scan('KWCOLOR') - return Literal(Color.from_hex(KWCOLOR, literal=True)) - else: # == 'KWVAR' - KWVAR = self._scan('KWVAR') - return Variable(KWVAR) - def interpolation(self): INTERP_START = self._scan('INTERP_START') expr_lst = self.expr_lst() @@ -451,40 +430,40 @@ class SassExpression(Parser): END = self._scan('END') return Interpolation.maybe(parts) + expr_map_or_list_rsts__ = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'COLOR', 'RPAR', 'BAREWORD', 'NUM', 'FNCT', 'VAR', 'BANG_IMPORTANT', 'SINGLE_QUOTE', '","']) u_expr_chks = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'COLOR', 'BAREWORD', 'NUM', 'FNCT', 'VAR', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) - m_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + m_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) argspec_items_rsts = set(['RPAR', 'END', '","']) - expr_map_rsts = set(['RPAR', '","']) - argspec_items_rsts__ = set(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'SIGN', 'VAR', 'ADD', 'NUM', '"url"', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) - kwatom_rsts = set(['KWVAR', 'KWID', 'KWSTR', 'KWQSTR', 'KWCOLOR', '":"', 'KWNUM']) + expr_slst_chks = set(['INTERP_END', 'RPAR', 'END', '":"', '","']) + expr_lst_rsts = set(['INTERP_END', 'END', '","']) + expr_map_or_list_rsts = set(['RPAR', '":"', '","']) argspec_item_chks = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'COLOR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) a_expr_chks = set(['ADD', 'SUB']) - expr_slst_rsts = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'END', 'COLOR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'NOT', 'INTERP_END', 'BANG_IMPORTANT', 'SINGLE_QUOTE', '","']) - interpolated_bareword_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) - or_expr_rsts = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'END', 'SINGLE_QUOTE', 'COLOR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'NOT', 'INTERP_END', 'BANG_IMPORTANT', 'OR', '","']) - interpolated_url_rsts = set(['DOUBLE_QUOTE', 'BAREURL', 'SINGLE_QUOTE']) + expr_slst_rsts = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'END', 'COLOR', 'FNCT', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', '":"', 'NOT', 'INTERP_END', 'BANG_IMPORTANT', 'SINGLE_QUOTE', '","']) + interpolated_bareword_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + or_expr_rsts = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'END', 'SINGLE_QUOTE', 'COLOR', 'FNCT', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', '":"', 'NOT', 'INTERP_END', 'BANG_IMPORTANT', 'OR', '","']) + argspec_chks_ = set(['END', 'RPAR']) interpolated_string_single_rsts = set(['SINGLE_QUOTE', 'INTERP_START']) - and_expr_rsts = set(['AND', 'LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'END', 'SINGLE_QUOTE', 'COLOR', 'RPAR', 'SIGN', 'VAR', 'ADD', 'NUM', '"url"', 'FNCT', 'NOT', 'INTERP_END', 'BANG_IMPORTANT', 'OR', '","']) - comparison_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'ADD', 'FNCT', 'VAR', 'EQ', 'AND', 'GE', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + and_expr_rsts = set(['AND', 'LPAR', 'RPAR', 'BAREWORD', 'END', 'SINGLE_QUOTE', 'COLOR', 'DOUBLE_QUOTE', 'FNCT', 'SIGN', 'VAR', 'ADD', 'NUM', '"url"', '":"', 'NOT', 'INTERP_END', 'BANG_IMPORTANT', 'OR', '","']) + comparison_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'ADD', 'FNCT', 'VAR', 'EQ', 'AND', 'GE', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) argspec_chks = set(['DOTDOTDOT', 'SLURPYVAR']) - atom_rsts_ = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'RPAR', 'VAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'UNITS', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + atom_rsts_ = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'RPAR', 'VAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'UNITS', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) interpolated_string_double_rsts = set(['DOUBLE_QUOTE', 'INTERP_START']) - expr_map_rsts_ = set(['KWVAR', 'KWID', 'KWSTR', 'KWQSTR', 'RPAR', 'KWCOLOR', '":"', 'KWNUM', '","']) + expr_map_or_list_rsts_ = set(['RPAR', '","']) u_expr_rsts = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'COLOR', 'SIGN', 'BAREWORD', 'ADD', 'NUM', 'FNCT', 'VAR', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) atom_chks = set(['COLOR', 'VAR']) + interpolated_url_rsts = set(['DOUBLE_QUOTE', 'BAREURL', 'SINGLE_QUOTE']) comparison_chks = set(['GT', 'GE', 'NE', 'LT', 'LE', 'EQ']) argspec_items_rsts_ = set(['KWVAR', 'LPAR', 'RPAR', 'BAREWORD', 'END', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'DOUBLE_QUOTE', 'SIGN', 'VAR', 'ADD', 'NUM', '"url"', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) - a_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + a_expr_rsts = set(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', '"url"', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) interpolated_string_rsts = set(['DOUBLE_QUOTE', 'SINGLE_QUOTE']) m_expr_chks = set(['MUL', 'DIV']) - kwatom_rsts_ = set(['UNITS', '":"']) goal_interpolated_anything_rsts = set(['END', 'INTERP_START']) interpolated_bare_url_rsts = set(['RPAR', 'INTERP_START']) - expr_lst_rsts = set(['INTERP_END', 'RPAR', 'END', '","']) argspec_items_chks = set(['KWVAR', '"url"', 'DOUBLE_QUOTE', 'BAREWORD', 'LPAR', 'COLOR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) argspec_rsts = set(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'END', 'SLURPYVAR', 'COLOR', 'BAREWORD', 'DOTDOTDOT', 'RPAR', 'VAR', 'ADD', 'NUM', '"url"', 'FNCT', 'NOT', 'SIGN', 'SINGLE_QUOTE']) - atom_rsts = set(['KWVAR', 'KWID', 'KWSTR', 'BANG_IMPORTANT', 'LPAR', 'COLOR', 'BAREWORD', 'KWQSTR', 'SIGN', 'DOUBLE_QUOTE', 'RPAR', 'KWCOLOR', 'VAR', 'ADD', 'NUM', '"url"', '":"', 'NOT', 'KWNUM', 'SINGLE_QUOTE', 'FNCT']) - argspec_chks_ = set(['END', 'RPAR']) + atom_rsts = set(['"url"', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'COLOR', 'BAREWORD', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'NOT', 'RPAR', 'SINGLE_QUOTE']) + argspec_items_rsts__ = set(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BAREWORD', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'SIGN', 'VAR', 'ADD', 'NUM', '"url"', 'FNCT', 'NOT', 'BANG_IMPORTANT', 'SINGLE_QUOTE']) argspec_rsts_ = set(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'END', 'COLOR', 'BAREWORD', 'SIGN', 'VAR', 'ADD', 'NUM', '"url"', 'FNCT', 'NOT', 'RPAR', 'SINGLE_QUOTE']) diff --git a/scss/types.py b/scss/types.py index 90faca1..65406df 100644 --- a/scss/types.py +++ b/scss/types.py @@ -1101,11 +1101,17 @@ class String(Value): # or at least that's what sass does. # Escape and add quotes as appropriate. if self.quotes is None: - return self._render_bareword() + # If you deliberately construct a bareword with bogus CSS in it, + # you're assumed to know what you're doing + return self.value else: return self._render_quoted() def _render_bareword(self): + # TODO this is currently unused, and only implemented due to an + # oversight, but would make for a much better implementation of + # escape() + # This is a bareword, so almost anything outside \w needs escaping ret = self.value ret = self.bad_identifier_rx.sub(self._escape_character, ret) @@ -1113,7 +1119,10 @@ class String(Value): # Also apply some minor quibbling rules about how barewords can # start: with a "name start", an escape, a hyphen followed by one # of those, or two hyphens. - if ret[0] == '-': + if not ret: + # TODO is an unquoted empty string allowed to be rendered? + pass + elif ret[0] == '-': if ret[1] in '-\\' or self._is_name_start(ret[1]): pass else: @@ -1132,10 +1141,11 @@ class String(Value): def _render_quoted(self): # Strictly speaking, the only things we need to quote are the quotes # themselves, backslashes, and newlines. - # Note: We ignore the original quotes and always use double quotes, to - # match Ruby Sass's behavior. This isn't particularly well-specified, - # though. - quote = '"' + # TODO Ruby Sass's behavior is to always use double quotes (and + # otherwise preserve the original literal in uncompressed mode) for + # computed strings, whereas we preserve a single quote in some cases + quote = self.quotes + ret = self.value ret = ret.replace('\\', '\\\\') ret = ret.replace(quote, '\\' + quote) @@ -1146,11 +1156,42 @@ class String(Value): return quote + ret + quote -class Url(String): +# TODO this needs to pretend the url(...) is part of the string for all string +# operations -- even the quotes! alas. +class Function(String): + """Function call pseudo-type, which crops up frequently in CSS as a string + marker. Acts mostly like a string, but has a function name and parentheses + around it. + """ + def __init__(self, string, function_name, quotes='"'): + super(Function, self).__init__(string, quotes=quotes) + self.function_name = function_name + + def render(self, compress=False): + return "{0}({1})".format( + self.function_name, + super(Function, self).render(compress), + ) + + +class Url(Function): + # Bare URLs may not contain quotes, parentheses, or unprintables. Quoted + # URLs may, of course, contain whatever they like. + # Ref: http://dev.w3.org/csswg/css-syntax-3/#consume-a-url-token0 + bad_identifier_rx = re.compile("[$'\"()\\x00-\\x08\\x0b\\x0e-\\x1f\\x7f]") + + def __init__(self, string, quotes=None): + super(Url, self).__init__(string, 'url', quotes=quotes) + def render(self, compress=False): - # TODO url-escape whatever needs escaping - # TODO does that mean we should un-url-escape when parsing? probably - return "url({0})".format(super(Url, self).render(compress)) + if self.quotes is None: + # Need to escape some stuff to keep this as valid CSS + inside = self.bad_identifier_rx.sub( + self._escape_character, self.value) + else: + inside = self._render_quoted() + + return "url(" + inside + ")" class Map(Value): |