From 4d7b19506b0edb3e232bebafd63ed5c5d4bc0508 Mon Sep 17 00:00:00 2001 From: Nathan Weizenbaum Date: Fri, 20 Nov 2009 03:40:42 -0800 Subject: [PATCH] [Sass] Add support for single-quoted strings. --- doc-src/SASS_CHANGELOG.md | 7 +++++++ doc-src/SASS_REFERENCE.md | 2 +- lib/sass/script/lexer.rb | 31 ++++++++++++++++++++++++++----- test/sass/script_test.rb | 18 +++++++++++++++--- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/doc-src/SASS_CHANGELOG.md b/doc-src/SASS_CHANGELOG.md index be570e9a..f3a13df8 100644 --- a/doc-src/SASS_CHANGELOG.md +++ b/doc-src/SASS_CHANGELOG.md @@ -29,6 +29,13 @@ For example: !prettiest-color = #542FA9 +### Single-Quoted Strings + +SassScript now supports single-quoted strings. +They behave identically to double-quoted strings, +except that single quotes need to be backslash-escaped +and double quotes do not. + ### Error Backtraces Numerous bugs were fixed with the backtraces given for Sass errors, diff --git a/doc-src/SASS_REFERENCE.md b/doc-src/SASS_REFERENCE.md index 7e3b24e1..d96745c1 100644 --- a/doc-src/SASS_REFERENCE.md +++ b/doc-src/SASS_REFERENCE.md @@ -691,7 +691,7 @@ available in that context. SassScript supports four data types: * numbers (e.g. `1.2`, `13`, `10px`) -* strings of text (e.g. `"foo"`, `"bar"`) +* strings of text (e.g. `"foo"`, `"bar"`, `'baz'`) * colors (e.g. `blue`, `#04a3f9`, `rgba(255, 0, 0, 0.5)`) * booleans (e.g. `true`, `false`) diff --git a/lib/sass/script/lexer.rb b/lib/sass/script/lexer.rb index 39ce3796..5ef1f230 100644 --- a/lib/sass/script/lexer.rb +++ b/lib/sass/script/lexer.rb @@ -54,13 +54,32 @@ module Sass :whitespace => /\s*/, :variable => /!([\w-]+)/, :ident => /(\\.|[^\s\\+*\/%(),=!])+/, - :string_end => /((?:\\.|\#(?!\{)|[^"\\#])*)(?:"|(?=#\{))/, :number => /(-)?(?:(\d*\.\d+)|(\d+))([a-zA-Z%]+)?/, :color => /\##{"([0-9a-fA-F]{1,2})" * 3}|(#{Color::HTML4_COLORS.keys.join("|")})(?!\()/, :bool => /(true|false)\b/, :op => %r{(#{Regexp.union(*OP_NAMES.map{|s| Regexp.new(Regexp.escape(s) + (s =~ /\w$/ ? '(?:\b|$)' : ''))})})} } + class << self + private + def string_re(open, close) + /#{open}((?:\\.|\#(?!\{)|[^#{close}\\#])*)(#{close}|(?=#\{))/ + end + end + + # A hash of regular expressions that are used for tokenizing strings. + # + # The key is a [Symbol, Boolean] pair. + # The symbol represents which style of quotation to use, + # while the boolean represents whether or not the string + # is following an interpolated segment. + STRING_REGULAR_EXPRESSIONS = { + [:double, false] => string_re('"', '"'), + [:single, false] => string_re("'", "'"), + [:double, true] => string_re('', '"'), + [:single, true] => string_re('', "'"), + } + # @param str [String, StringScanner] The source text to lex # @param line [Fixnum] The line on which the SassScript appears. # Used for error reporting @@ -71,6 +90,7 @@ module Sass @line = line @offset = offset @filename = filename + @interpolation_stack = [] @prev = nil end @@ -114,8 +134,8 @@ module Sass end def token - return string('') if after_interpolation? - variable || string || number || color || bool || op || ident + return string(@interpolation_stack.pop, true) if after_interpolation? + variable || string(:double, false) || string(:single, false) || number || color || bool || op || ident end def variable @@ -128,8 +148,9 @@ module Sass [:ident, s.gsub(/\\(.)/, '\1')] end - def string(start_char = '"') - return unless @scanner.scan(/#{start_char}#{REGULAR_EXPRESSIONS[:string_end]}/) + def string(re, open) + return unless @scanner.scan(STRING_REGULAR_EXPRESSIONS[[re, open]]) + @interpolation_stack << re if @scanner[2].empty? # Started an interpolated section [:string, Script::String.new(@scanner[1].gsub(/\\([^0-9a-f])/, '\1').gsub(/\\([0-9a-f]{1,4})/, "\\\\\\1"))] end diff --git a/test/sass/script_test.rb b/test/sass/script_test.rb index 767792fc..2a0f8289 100644 --- a/test/sass/script_test.rb +++ b/test/sass/script_test.rb @@ -16,9 +16,21 @@ class SassScriptTest < Test::Unit::TestCase end def test_string_escapes + assert_equal "'", resolve("\"'\"") assert_equal '"', resolve("\"\\\"\"") assert_equal "\\", resolve("\"\\\\\"") assert_equal "\\02fa", resolve("\"\\02fa\"") + + assert_equal "'", resolve("'\\''") + assert_equal '"', resolve("'\"'") + assert_equal "\\", resolve("'\\\\'") + assert_equal "\\02fa", resolve("'\\02fa'") + end + + def test_string_interpolation + assert_equal "foo2bar", resolve('\'foo#{1 + 1}bar\'') + assert_equal "foo2bar", resolve('"foo#{1 + 1}bar"') + assert_equal "foo1bar5baz4bang", resolve('\'foo#{1 + "bar#{2 + 3}baz" + 4}bang\'') end def test_color_names @@ -188,16 +200,16 @@ WARN def test_string_ops assert_equal "foo bar", resolve('"foo" "bar"') assert_equal "true 1", resolve('true 1') - assert_equal "foo, bar", resolve('"foo" , "bar"') + assert_equal "foo, bar", resolve("'foo' , 'bar'") assert_equal "true, 1", resolve('true , 1') assert_equal "foobar", resolve('"foo" + "bar"') assert_equal "true1", resolve('true + 1') - assert_equal "foo-bar", resolve('"foo" - "bar"') + assert_equal "foo-bar", resolve("'foo' - 'bar'") assert_equal "true-1", resolve('true - 1') assert_equal "foo/bar", resolve('"foo" / "bar"') assert_equal "true/1", resolve('true / 1') - assert_equal "-bar", resolve('- "bar"') + assert_equal "-bar", resolve("- 'bar'") assert_equal "-true", resolve('- true') assert_equal "/bar", resolve('/ "bar"') assert_equal "/true", resolve('/ true')