summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/back-matter.rst1
-rw-r--r--docs/syntax.rst30
-rw-r--r--docs/usage.rst4
-rw-r--r--scss/expression.py2
-rw-r--r--scss/functions/core.py88
-rw-r--r--scss/tests/functions/test_core.py52
-rw-r--r--scss/types.py23
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