Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String functions #401

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
141 changes: 141 additions & 0 deletions lib/sass/script/functions.rb
Expand Up @@ -110,6 +110,21 @@ module Sass::Script
# \{#quote quote($string)}
# : Adds quotes to a string.
#
# \{#str_length str-length($string)}
# : Returns the number of characters in a string.
#
# \{#str_insert str-insert($string, $insert, $index)}
# : Inserts a string $insert into $string at the specified $index.
#
# \{#str_extract str-extract($string, $start, $end)}
# : Extracts a substring of characters from $string
#
# \{#to_upper_case to-upper-case($string)}
# : Converts a string to upper case.
#
# \{#to_lower_case to-lower-case($string)}
# : Converts a string to lower case.
#
# ## Number Functions
#
# \{#percentage percentage($value)}
Expand Down Expand Up @@ -1078,6 +1093,132 @@ def quote(string)
end
declare :quote, [:string]

# Returns the number of characters in a string.
#
# @return [Sass::Script::Number]
# @raise [ArgumentError] if `string` isn't a string
# @example
# str-length("foo") => 3
def str_length(string)
assert_type string, :String
Sass::Script::Number.new(string.value.size)
end
declare :str_length, [:string]

# inserts a string into another string
#
# Inserts the `insert` string before the character at the given index.
# Negative indices count from the end of the string.
# The inserted string will starts at the given index.
#
# @return [Sass::Script::String]
# @raise [ArgumentError] if `original` isn't a string, `insert` isn't a string, or `index` isn't a number.
# @example
# str-insert("abcd", "X", 1) => "Xabcd"
# str-insert("abcd", "X", 4) => "abcXd"
# str-insert("abcd", "X", 100) => "abcdX"
# str-insert("abcd", "X", -100) => "Xabcd"
# str-insert("abcd", "X", -4) => "aXbcd"
# str-insert("abcd", "X", -1) => "abcdX"
def str_insert(original, insert, index)
assert_type original, :String
assert_type insert, :String
assert_type index, :Number
unless index.unitless?
raise ArgumentError.new("#{index.inspect} is not a unitless number")
end
insertion_point = index.value > 0 ? [index.value - 1, original.value.size].min : [index.value, -original.value.size - 1].max
Sass::Script::String.new(original.value.dup.insert(insertion_point, insert.value), original.type)
end
declare :str_insert, [:original, :insert, :index]

# Starting at the left, finds the index of the first location
# where `substring` is found in `string`.
#
# @return [Sass::Script::String]
# @raise [ArgumentError] if `original` isn't a string, `insert` isn't a string, or `index` isn't a number.
# @example
# str-index(abcd, a) => 1
# str-index(abcd, ab) => 1
# str-index(abcd, X) => 0
# str-index(abcd, c) => 3
def str_index(string, substring)
assert_type string, :String
assert_type substring, :String
index = string.value.index(substring.value) || -1
Sass::Script::Number.new(index + 1)
end
declare :str_index, [:string, :substring]


# Extract a substring from `string` from `start` index to `end` index.
#
# @return [Sass::Script::String]
# @raise [ArgumentError] if `string` isn't a string or `start` and `end` aren't unitless numbers
# @example
# str-extract(abcd,2,3) => bc
# str-extract(abcd,2) => cd
# str-extract(abcd,-2) => abc
# str-extract(abcd,2,-2) => bc
# str-extract(abcd,3,-3) => unquote("")
# str-extract("abcd",3,-3) => ""
# str-extract(abcd,1,1) => a
# str-extract(abcd,1,2) => ab
# str-extract(abcd,1,4) => abcd
# str-extract(abcd,-100,4) => abcd
# str-extract(abcd,1,100) => abcd
# str-extract(abcd,2,1) => unquote("")
# str-extract("abcd",2,3) => "bc"
def str_extract(string, start_at, end_at = nil)
assert_type string, :String
assert_type start_at, :Number
unless start_at.unitless?
raise ArgumentError.new("#{start_at.inspect} is not a unitless number")
end
if end_at.nil?
if start_at.value < 0
end_at = start_at
start_at = Sass::Script::Number.new(1)
else
end_at = Sass::Script::Number.new(-1)
end
end
assert_type end_at, :Number
unless end_at.unitless?
raise ArgumentError.new("#{end_at.inspect} is not a unitless number")
end
s = start_at.value > 0 ? start_at.value - 1 : start_at.value
e = end_at.value > 0 ? end_at.value - 1 : end_at.value
extracted = string.value.slice(s..e)
Sass::Script::String.new(extracted || "", string.type)
end
declare :str_index, [:string, :start, :end]
# Convert a string to upper case
#
# @return [Sass::Script::String]
# @raise [ArgumentError] if `string` isn't a string
# @example
# to-upper-case(abcd) => ABCD
# to-upper-case("abcd") => "ABCD"
def to_upper_case(string)
assert_type string, :String
Sass::Script::String.new(string.value.upcase, string.type)
end
declare :to_upper_case, [:string]

# Convert a string to lower case
#
# @return [Sass::Script::String]
# @raise [ArgumentError] if `string` isn't a string
# @example
# to-lower-case(ABCD) => abcd
# to-lower-case("ABCD") => "abcd"
def to_lower_case(string)
assert_type string, :String
Sass::Script::String.new(string.value.downcase, string.type)
end
declare :to_lower_case, [:string]

# Inspects the type of the argument, returning it as an unquoted string.
#
# @example
Expand Down
76 changes: 76 additions & 0 deletions test/sass/functions_test.rb
Expand Up @@ -850,6 +850,82 @@ def test_quote_tests_type
assert_error_message("#ff0000 is not a string for `quote'", "quote(#f00)")
end

def test_str_length
assert_equal('3', evaluate('str-length(foo)'))
end

def test_str_length_requires_a_string
assert_error_message("#ff0000 is not a string for `str-length'", "str-length(#f00)")
end

def test_str_insert
assert_equal('Xabcd', evaluate('str-insert(abcd, X, 0)'))
assert_equal('Xabcd', evaluate('str-insert(abcd, X, 1)'))
assert_equal('abcXd', evaluate('str-insert(abcd, X, 4)'))
assert_equal('abcdX', evaluate('str-insert(abcd, X, 100)'))
assert_equal('Xabcd', evaluate('str-insert(abcd, X, -100)'))
assert_equal('aXbcd', evaluate('str-insert(abcd, X, -4)'))
assert_equal('abcdX', evaluate('str-insert(abcd, X, -1)'))
end

def test_str_insert_maintains_quote_of_primary_string
assert_equal('"Xfoo"', evaluate('str-insert("foo", X, 1)'))
assert_equal('"Xfoo"', evaluate('str-insert("foo", "X", 1)'))
assert_equal('Xfoo', evaluate('str-insert(foo, "X", 1)'))
end

def test_str_insert_asserts_types
assert_error_message("#ff0000 is not a string for `str-insert'", "str-insert(#f00, X, 1)")
assert_error_message("#ff0000 is not a string for `str-insert'", "str-insert(foo, #f00, 1)")
assert_error_message("#ff0000 is not a number for `str-insert'", "str-insert(foo, X, #f00)")
assert_error_message("10px is not a unitless number for `str-insert'", "str-insert(foo, X, 10px)")
end

def test_str_index
assert_equal('1', evaluate('str-index(abcd, a)'))
assert_equal('1', evaluate('str-index(abcd, ab)'))
assert_equal('0', evaluate('str-index(abcd, X)'))
assert_equal('3', evaluate('str-index(abcd, c)'))
end

def test_str_index_asserts_types
assert_error_message("#ff0000 is not a string for `str-index'", "str-index(#f00, X)")
assert_error_message("#ff0000 is not a string for `str-index'", "str-index(asdf, #f00)")
end

def test_to_lower_case
assert_equal('abcd', evaluate('to-lower-case(ABCD)'))
assert_equal('"abcd"', evaluate('to-lower-case("ABCD")'))
assert_error_message("#ff0000 is not a string for `to-lower-case'", "to-lower-case(#f00)")
end

def test_to_upper_case
assert_equal('ABCD', evaluate('to-upper-case(abcd)'))
assert_equal('"ABCD"', evaluate('to-upper-case("abcd")'))
assert_error_message("#ff0000 is not a string for `to-upper-case'", "to-upper-case(#f00)")
end

def test_str_extract
assert_equal('bc', evaluate('str-extract(abcd,2,3)')) # in the middle of the string
assert_equal('a', evaluate('str-extract(abcd,1,1)')) # when start = end
assert_equal('ab', evaluate('str-extract(abcd,1,2)')) # for completeness
assert_equal('abcd', evaluate('str-extract(abcd,1,4)')) # at the end points
assert_equal('abcd', evaluate('str-extract(abcd,0,4)')) # when start is before the start of the string
assert_equal('abcd', evaluate('str-extract(abcd,1,100)')) # when end is past the end of the string
assert_equal('', evaluate('str-extract(abcd,2,1)')) # when end is before start
assert_equal('"bc"', evaluate('str-extract("abcd",2,3)')) # when used with a quoted string
assert_equal('bcd', evaluate('str-extract(abcd,2)')) # when end is omitted, you get the remainder of the string
assert_equal('abc', evaluate('str-extract(abcd,-2)')) # when end is omitted, and start is negative you get the start of the string
assert_equal('bc', evaluate('str-extract(abcd,2,-2)')) # when end is negative it counts in from the end
assert_equal('', evaluate('str-extract(abcd,3,-3)')) # when end is negative and comes before the start
assert_equal('bc', evaluate('str-extract(abcd,-3,-2)')) # when both are negative
assert_error_message("#ff0000 is not a string for `str-extract'", "str-extract(#f00,2,3)")
assert_error_message("#ff0000 is not a number for `str-extract'", "str-extract(abcd,#f00,3)")
assert_error_message("#ff0000 is not a number for `str-extract'", "str-extract(abcd,2,#f00)")
assert_error_message("3px is not a unitless number for `str-extract'", "str-extract(abcd,2,3px)")
assert_error_message("2px is not a unitless number for `str-extract'", "str-extract(abcd,2px,3)")
end

def test_user_defined_function
assert_equal("I'm a user-defined string!", evaluate("user_defined()"))
end
Expand Down