summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2014-08-29 19:48:37 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2014-08-29 19:48:37 -0700
commite4eb845498108f69e0b6f72eacb6e94aed6b120f (patch)
tree3a01688995bdd558d9d53a70640c3e6c1368b0e3
parenta42962221824dfe7af5067ca05da8a2858fc0dbf (diff)
downloadpyscss-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.py30
-rw-r--r--scss/extension/compass/images.py9
-rw-r--r--scss/extension/fonts.py16
-rw-r--r--scss/grammar/expression.g47
-rw-r--r--scss/grammar/expression.py111
-rw-r--r--scss/types.py61
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):