diff options
-rw-r--r-- | docs/back-matter.rst | 1 | ||||
-rw-r--r-- | docs/syntax.rst | 30 | ||||
-rw-r--r-- | docs/usage.rst | 4 | ||||
-rw-r--r-- | scss/expression.py | 2 | ||||
-rw-r--r-- | scss/functions/core.py | 88 | ||||
-rw-r--r-- | scss/tests/functions/test_core.py | 52 | ||||
-rw-r--r-- | scss/types.py | 23 |
7 files changed, 193 insertions, 7 deletions
diff --git a/docs/back-matter.rst b/docs/back-matter.rst index 1fcdb8d..f2c98fc 100644 --- a/docs/back-matter.rst +++ b/docs/back-matter.rst @@ -113,6 +113,7 @@ New features * Python 3 support. As a result, Python 2.5 no longer works; whether this is a bug or a feature is not yet clear. * It's possible to write custom Sass functions in Python, though the API for this is not final. * Experimental support for the map type and destructuring ``@each``, both unreleased additions to the Ruby project. +* Support for the new string and list functions in Sass 3.3. * Added ``background-brushed``. Backwards-incompatible changes diff --git a/docs/syntax.rst b/docs/syntax.rst index 4cd21c9..26954d2 100644 --- a/docs/syntax.rst +++ b/docs/syntax.rst @@ -7,8 +7,9 @@ pyScss syntax Supported Sass features ======================= -pyScss is mostly compatible with Sass 3.2. The canonical syntax reference is -in the Sass documentation: +pyScss is mostly compatible with Sass 3.2 and has partial support for the +upcoming Sass 3.3. The canonical syntax reference is in the Sass +documentation: http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html @@ -22,7 +23,8 @@ for the SASS (YAML-like) syntax. Built-in functions ------------------ -All of the functions described in `the Sass documentation`_ are supported. +All of the Sass 3.2 functions described in `the Sass documentation`_ are +supported. .. _the Sass documentation: <http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html @@ -460,3 +462,25 @@ considered a bug. The ``@while`` construct doesn't work at all and will be left intact in the output, like any other unrecognized ``@``-rule. + + +CLI +--- + +pyScss's command-line arguments are not entirely compatible with those of the +reference compiler. + + +Sass 3.3 +-------- + +The following Sass 3.3 improvements are not yet implemented, but are planned +for the near future: + +* Use of ``&`` in expressions. +* ``@at-root`` +* Source map support. +* Using ``...`` multiple times in a function call, or passing a map of + arguments with ``...``. Likewise, ``keywords()`` is not implemented. +* ``unique-id()``, ``call()``, and the various ``*-exists()`` functions are not + implemented. diff --git a/docs/usage.rst b/docs/usage.rst index 364770a..5287298 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -4,8 +4,8 @@ Installation and usage Installation ------------ -pyScss requires only Python 2.5 or later, including Python 3.x. Install with -pip:: +pyScss requires only Python 2.5 or later, including Python 3.x. PyPy is also +known to work. Install with pip:: pip install pyScss diff --git a/scss/expression.py b/scss/expression.py index cacd8ec..32af41d 100644 --- a/scss/expression.py +++ b/scss/expression.py @@ -304,7 +304,7 @@ class CallOp(Expression): try: # DEVIATION: Fall back to single parameter funct = calculator.namespace.function(func_name, 1) - args = [args] + args = [List(args, use_comma=True)] except KeyError: if not is_builtin_css_function(func_name): log.error("Function not found: %s:%s", func_name, argspec_len, extra={'stack': True}) diff --git a/scss/functions/core.py b/scss/functions/core.py index 9a68cc8..7a99b12 100644 --- a/scss/functions/core.py +++ b/scss/functions/core.py @@ -431,7 +431,7 @@ def change_color(color, red=None, green=None, blue=None, hue=None, saturation=No # ------------------------------------------------------------------------------ -# String type manipulation +# String functions @register('e', 1) @register('escape', 1) @@ -455,6 +455,73 @@ def quote(*args): return String(arg.render(), quotes='"') +@register('str-length', 1) +def str_length(string): + expect_type(string, String) + + # nb: can't use `len(string)`, because that gives the Sass list length, + # which is 1 + return Number(len(string.value)) + + +# TODO this and several others should probably also require integers +# TODO and assert that the indexes are valid +@register('str-insert', 3) +def str_insert(string, insert, index): + expect_type(string, String) + expect_type(insert, String) + expect_type(index, Number, unit=None) + + py_index = index.to_python_index(len(string.value), check_bounds=False) + return String( + string.value[:py_index] + + insert.value + + string.value[py_index:], + quotes=string.quotes) + + +@register('str-index', 2) +def str_index(string, substring): + expect_type(string, String) + expect_type(substring, String) + + # 1-based indexing, with 0 for failure + return Number(string.value.find(substring.value) + 1) + + +@register('str-slice', 2) +@register('str-slice', 3) +def str_slice(string, start_at, end_at=None): + expect_type(string, String) + expect_type(start_at, Number, unit=None) + py_start_at = start_at.to_python_index(len(string.value)) + + if end_at is None: + py_end_at = None + else: + expect_type(end_at, Number, unit=None) + # Endpoint is inclusive, unlike Python + py_end_at = end_at.to_python_index(len(string.value)) + 1 + + return String( + string.value[py_start_at:py_end_at], + quotes=string.quotes) + + +@register('to-upper-case', 1) +def to_upper_case(string): + expect_type(string, String) + + return String(string.value.upper(), quotes=string.quotes) + + +@register('to-lower-case', 1) +def to_lower_case(string): + expect_type(string, String) + + return String(string.value.lower(), quotes=string.quotes) + + # ------------------------------------------------------------------------------ # Number functions @@ -574,6 +641,25 @@ def zip_(*lists): use_comma=True) +# TODO need a way to use "list" as the arg name without shadowing the builtin +@register('list-separator', 1) +def list_separator(list): + if list.use_comma: + return String.unquoted('comma') + else: + return String.unquoted('space') + + +@register('set-nth', 3) +def set_nth(list, n, value): + expect_type(n, Number, unit=None) + + py_n = n.to_python_index(len(list)) + return List( + tuple(list[:py_n]) + (value,) + tuple(list[py_n + 1:]), + use_comma=list.use_comma) + + # ------------------------------------------------------------------------------ # Map functions diff --git a/scss/tests/functions/test_core.py b/scss/tests/functions/test_core.py index 4b1e906..4f886c9 100644 --- a/scss/tests/functions/test_core.py +++ b/scss/tests/functions/test_core.py @@ -246,6 +246,46 @@ def test_quote(calc): assert ret.quotes == '"' +# TODO more of these need quote checking too +def test_str_length(calc): + # Examples from the Ruby docs + assert calc('str-length("foo")') == calc('3') + + +def test_str_insert(calc): + # Examples from the Ruby docs + assert calc('str-insert("abcd", "X", 1)') == calc('"Xabcd"') + assert calc('str-insert("abcd", "X", 4)') == calc('"abcXd"') + # DEVIATION: see https://github.com/nex3/sass/issues/954 + assert calc('str-insert("abcd", "X", 5)') == calc('"abcdX"') + + +def test_str_index(calc): + # Examples from the Ruby docs + assert calc('str-index(abcd, a)') == calc('1') + assert calc('str-index(abcd, ab)') == calc('1') + assert calc('str-index(abcd, X)') == calc('0') + assert calc('str-index(abcd, c)') == calc('3') + + +def test_str_slice(calc): + # Examples from the Ruby docs + assert calc('str-slice("abcd", 2, 3)') == calc('"bc"') + assert calc('str-slice("abcd", 2)') == calc('"bcd"') + assert calc('str-slice("abcd", -3, -2)') == calc('"bc"') + assert calc('str-slice("abcd", 2, -2)') == calc('"bc"') + + +def test_to_upper_case(calc): + # Examples from the Ruby docs + assert calc('to-upper-case(abcd)') == calc('ABCD') + + +def test_to_lower_case(calc): + # Examples from the Ruby docs + assert calc('to-lower-case(ABCD)') == calc('abcd') + + # ------------------------------------------------------------------------------ # Number functions @@ -337,6 +377,18 @@ def test_index(calc): assert calc('index(1px solid red, dashed)') == calc('false') +def test_list_separator(calc): + # Examples from the Ruby docs + assert calc('list-separator(1px 2px 3px)') == calc('space') + assert calc('list-separator(1px, 2px, 3px)') == calc('comma') + assert calc('list-separator("foo")') == calc('space') + + +def test_set_nth(calc): + # Examples from the Ruby docs + assert calc('set-nth($list: 10px 20px 30px, $n: 2, $value: -20px)') == calc('10px -20px 30px') + + # ------------------------------------------------------------------------------ # Map functions diff --git a/scss/types.py b/scss/types.py index 7ef6dc3..f6e6c10 100644 --- a/scss/types.py +++ b/scss/types.py @@ -447,6 +447,29 @@ class Number(Value): return wrapped + def to_python_index(self, length, check_bounds=True): + """Return a plain Python integer appropriate for indexing a sequence of + the given length. Raise if this is impossible for any reason + whatsoever. + """ + if not self.is_unitless: + raise ValueError("Index cannot have units: {0!r}".format(self)) + + ret = int(self.value) + if ret != self.value: + raise ValueError("Index must be an integer: {0!r}".format(ret)) + + if ret == 0: + raise ValueError("Index cannot be zero") + + if check_bounds and abs(ret) > length: + raise ValueError("Index {0!r} out of bounds for length {1}".format(ret, length)) + + if ret > 0: + return ret - 1 + else: + return ret + @property def has_simple_unit(self): """Returns True iff the unit is expressible in CSS, i.e., has no |