Add null literal. #374

Closed
wants to merge 2 commits into
from
Jump to file or symbol
Failed to load files and symbols.
+195 −10
Diff settings

Always

Just for now

View
@@ -627,12 +627,13 @@ this still works, but it's deprecated and prints a warning.
### Data Types
-SassScript supports four main data types:
+SassScript supports six main data types:
* numbers (e.g. `1.2`, `13`, `10px`)
* strings of text, with and without quotes (e.g. `"foo"`, `'bar'`, `baz`)
* colors (e.g. `blue`, `#04a3f9`, `rgba(255, 0, 0, 0.5)`)
* booleans (e.g. `true`, `false`)
+* nulls, which are falsey (e.g. `null`)
* lists of values, separated by spaces or commas (e.g. `1.5em 1em 0 2em`, `Helvetica, Arial, sans-serif`)
SassScript also supports all other types of CSS property value,
@@ -710,6 +711,8 @@ They can't be output directly to CSS;
if you try to do e.g. `font-family: ()`, Sass will raise an error.
If a list contains empty lists, as in `1px 2px () 3px`,
the empty list will be removed before it's turned into CSS.
+Null values are also be removed from lists, so `1px 2px null 3px`
+would be become `1px 2px 3px` in CSS.
### Operations
@@ -932,6 +935,20 @@ is compiled to:
p:before {
content: "I ate 15 pies!"; }
+Variables with a null value are treated as an empty string in
+string operations & interpolations:
+
+ $value: null;
+ p:before {
+ content: "I ate #{$value} pies!";
+ font-family: sans- + $value; }
+
+is compiled to:
+
+ p:before {
+ content: "I ate pies!";
+ font-family: sans-; }
+
#### Boolean Operations
SassScript supports `and`, `or`, and `not` operators
@@ -1046,6 +1063,21 @@ is compiled to:
content: "First content";
new-content: "First time reference"; }
+If variables are set to a `null` value, !default will treat
+them like they're unassigned:
+
+ $content: null;
+ $content: "Non-null content" !default;
+
+ #main {
+ content: $content;
+ }
+
+is compiled to:
+
+ #main {
+ content: "Non-null content"; }
+
## `@`-Rules and Directives {#directives}
Sass supports all CSS3 `@`-rules,
View
@@ -90,6 +90,7 @@ class Lexer
:number => /(-)?(?:(\d*\.\d+)|(\d+))([a-zA-Z%]+)?/,
:color => HEXCOLOR,
:bool => /(true|false)\b/,
+ :null => /null\b/,
:ident_op => %r{(#{Regexp.union(*IDENT_OP_NAMES.map{|s| Regexp.new(Regexp.escape(s) + "(?!#{NMCHAR}|\Z)")})})},
:op => %r{(#{Regexp.union(*OP_NAMES)})},
}
@@ -234,7 +235,7 @@ def token
end
variable || string(:double, false) || string(:single, false) || number ||
- color || bool || string(:uri, false) || raw(UNICODERANGE) ||
+ color || bool || null || string(:uri, false) || raw(UNICODERANGE) ||
special_fun || special_val || ident_op || ident || op
end
@@ -292,6 +293,11 @@ def bool
[:bool, Script::Bool.new(s == 'true')]
end
+ def null
+ return unless scan(REGULAR_EXPRESSIONS[:null])
+ [:null, Script::Null.new]
+ end
+
def special_fun
return unless str1 = scan(/((-[\w-]+-)?(calc|element)|expression|progid:[a-z\.]*)\(/i)
str2, _ = Sass::Shared.balance(@scanner, ?(, ?), 1)
View
@@ -41,14 +41,14 @@ def eq(other)
# @see Node#to_s
def to_s(opts = {})
raise Sass::SyntaxError.new("() isn't a valid CSS value.") if value.empty?
- return value.reject {|e| e.is_a?(List) && e.value.empty?}.map {|e| e.to_s(opts)}.join(sep_str)
+ return value.reject {|e| e.is_a?(Null) || e.is_a?(List) && e.value.empty?}.map {|e| e.to_s(opts)}.join(sep_str)
end
# @see Node#to_sass
def to_sass(opts = {})
return "()" if value.empty?
precedence = Sass::Script::Parser.precedence_of(separator)
- value.map do |v|
+ value.reject {|e| e.is_a?(Null)}.map do |v|
if v.is_a?(List) && Sass::Script::Parser.precedence_of(v.separator) <= precedence
"(#{v.to_sass(opts)})"
else
@@ -9,6 +9,7 @@ class Literal < Node
require 'sass/script/number'
require 'sass/script/color'
require 'sass/script/bool'
+ require 'sass/script/null'
require 'sass/script/list'
# Returns the Ruby value of the literal.
View
@@ -0,0 +1,34 @@
+require 'sass/script/literal'
+
+module Sass::Script
+ # A SassScript object representing a null value.
+ class Null < Literal
+ # Creates a new null literal.
+ def initialize
+ super nil
+ end
+
+ # @return [Boolean] `false` (the Ruby boolean value)
+ def to_bool
+ false
+ end
+
+ # @return [Boolean] `true`
+ def nil?
+ true
+ end
+
+ # @return [String] '' (An empty string)
+ def to_s(opts = {})
+ ''
+ end
+ alias_method :to_sass, :to_s
+
+ # Returns a string representing a null value.
+ #
+ # @return [String]
+ def inspect
+ 'null'
+ end
+ end
+end
@@ -82,6 +82,11 @@ def _perform(environment)
literal2 = @operand2.perform(environment)
+ literal_types = [literal1.class, literal2.class]
+ if !literal_types.include?(String) && literal_types.include?(Null)
+ raise Sass::SyntaxError.new("Invalid null operation: \"#{literal1.inspect} #{@operator} #{literal2.inspect}\".")
+ end
+
begin
opts(literal1.send(@operator, literal2))
rescue NoMethodError => e
@@ -453,7 +453,7 @@ def number
end
def literal
- (t = try_tok(:color, :bool)) && (return t.value)
+ (t = try_tok(:color, :bool, :null)) && (return t.value)
end
# It would be possible to have unified #assert and #try methods,
@@ -98,7 +98,7 @@ def check!
if @options[:property_syntax] && @options[:property_syntax] != @prop_syntax
raise Sass::SyntaxError.new(
"Illegal property syntax: can't use #{@prop_syntax} syntax when :property_syntax => #{@options[:property_syntax].inspect} is set.")
- elsif resolved_value.empty?
+ elsif value.is_a?(Sass::Script::String) && value.to_s.empty?
raise Sass::SyntaxError.new("Invalid property: #{declaration.dump} (no value)." +
pseudo_class_selector_message)
end
@@ -122,6 +122,7 @@ def visit_cssimport(node)
end
def visit_prop(node)
+ return if node.resolved_value =~ /\A[\s'"()]*\Z/
tab_str = ' ' * (@tabs + node.tabs)
if node.style == :compressed
"#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
@@ -188,13 +189,13 @@ def visit_rule(node)
end
if node.style == :compact
- properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(' ')}
+ properties = with_tabs(0) {node.children.map {|a| visit(a)}.compact.join(' ')}
to_return << "#{total_rule} { #{properties} }#{"\n" if node.group_end}"
elsif node.style == :compressed
- properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(';')}
+ properties = with_tabs(0) {node.children.map {|a| visit(a)}.compact.join(';')}
to_return << "#{total_rule}{#{properties}}"
else
- properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.join("\n")}
+ properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.compact.join("\n")}
end_props = (node.style == :expanded ? "\n" + old_spaces : ' ')
to_return << "#{total_rule} {\n#{properties}#{end_props}}#{"\n" if node.group_end}"
end
View
@@ -1011,7 +1011,7 @@ def test_debug_info
def test_debug_info_without_filename
assert_equal(<<CSS, Sass::Engine.new(<<SASS, :debug_info => true).render)
-@media -sass-debug-info{filename{font-family:}line{font-family:\\000031}}
+@media -sass-debug-info{filename{}line{font-family:\\000031}}
foo {
a: b; }
CSS
@@ -1100,6 +1100,7 @@ def test_property_with_content_and_nested_props
def test_guarded_assign
assert_equal("foo {\n a: b; }\n", render(%Q{$foo: b\n$foo: c !default\nfoo\n a: $foo}))
assert_equal("foo {\n a: b; }\n", render(%Q{$foo: b !default\nfoo\n a: $foo}))
+ assert_equal("foo {\n a: b; }\n", render(%Q{$foo: null\n$foo: b !default\nfoo\n a: $foo}))
end
def test_mixins
@@ -1185,6 +1186,35 @@ def test_default_values_for_mixin_arguments
+foo(#fff, 2px)
three
+foo(#fff, 2px, 3px)
+SASS
+ assert_equal(<<CSS, render(<<SASS))
+one {
+ color: white;
+ padding: 1px;
+ margin: 4px; }
+
+two {
+ color: white;
+ padding: 2px;
+ margin: 5px; }
+
+three {
+ color: white;
+ padding: 2px;
+ margin: 3px; }
+CSS
+$a: 5px
+=foo($a, $b: 1px, $c: null)
+ $c: 3px + $b !default
+ :color $a
+ :padding $b
+ :margin $c
+one
+ +foo(#fff)
+two
+ +foo(#fff, 2px)
+three
+ +foo(#fff, 2px, 3px)
SASS
end
@@ -1743,6 +1773,22 @@ def test_empty_selector_warning
END
end
+ def test_nonprinting_empty_property
+ assert_equal(<<CSS, render(<<SASS))
+a {
+ e: f; }
+CSS
+$null-value: null
+$empty-string: ''
+$empty-list: (null)
+a
+ b: $null-value
+ c: $empty-string
+ d: $empty-list
+ e: f
+SASS
+ end
+
def test_root_level_pseudo_class_with_new_properties
assert_equal(<<CSS, render(<<SASS, :property_syntax => :new))
:focus {
@@ -871,6 +871,7 @@ def test_type_of
assert_equal("bool", evaluate("type-of(true)"))
assert_equal("color", evaluate("type-of(#fff)"))
assert_equal("color", evaluate("type-of($value: #fff)"))
+ assert_equal("null", evaluate("type-of(null)"))
end
def test_unit
@@ -1011,6 +1012,7 @@ def test_index
def test_if
assert_equal("1px", evaluate("if(true, 1px, 2px)"))
assert_equal("2px", evaluate("if(false, 1px, 2px)"))
+ assert_equal("2px", evaluate("if(null, 1px, 2px)"))
end
def test_keyword_args_rgb
View
@@ -263,6 +263,10 @@ def test_booleans
assert_equal "false", resolve("false")
end
+ def test_null
+ assert_equal "", resolve("null")
+ end
+
def test_boolean_ops
assert_equal "true", resolve("true and true")
assert_equal "true", resolve("false or true")
@@ -281,6 +285,18 @@ def test_boolean_ops
assert_equal "false", resolve("false and 1")
assert_equal "2", resolve("2 or 3")
assert_equal "3", resolve("2 and 3")
+
+ assert_equal "true", resolve("null or true")
+ assert_equal "true", resolve("true or null")
+ assert_equal "", resolve("null or null")
+ assert_equal "", resolve("null and true")
+ assert_equal "", resolve("true and null")
+ assert_equal "", resolve("null and null")
+
+ assert_equal "true", resolve("not null")
+
+ assert_equal "1", resolve("null or 1")
+ assert_equal "", resolve("null and 1")
end
def test_arithmetic_ops
@@ -304,6 +320,12 @@ def test_string_ops
assert_equal "true-1", resolve('true - 1')
assert_equal '"foo"/"bar"', resolve('"foo" / "bar"')
assert_equal "true/1", resolve('true / 1')
+ assert_equal "foo", resolve('"foo" + null')
+ assert_equal "foo", resolve('null + "foo"')
+ assert_equal '"foo"-', resolve('"foo" - null')
+ assert_equal '-"foo"', resolve('null - "foo"')
+ assert_equal '"foo"/', resolve('"foo" / null')
+ assert_equal '/"foo"', resolve('null / "foo"')
assert_equal '-"bar"', resolve("- 'bar'")
assert_equal "-true", resolve('- true')
@@ -326,6 +348,33 @@ def test_relational_ops
assert_equal "false", resolve("3 <= 2")
end
+ def test_null_ops
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "null plus 1".') {eval("null + 1")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "null minus 1".') {eval("null - 1")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "null times 1".') {eval("null * 1")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "null div 1".') {eval("null / 1")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "null mod 1".') {eval("null % 1")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "1 plus null".') {eval("1 + null")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "1 minus null".') {eval("1 - null")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "1 times null".') {eval("1 * null")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "1 div null".') {eval("1 / null")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "1 mod null".') {eval("1 % null")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "1 gt null".') {eval("1 > null")}
+ assert_raise_message(Sass::SyntaxError,
+ 'Invalid null operation: "null lt 1".') {eval("null < 1")}
+ end
+
def test_equals
assert_equal("true", resolve('"foo" == $foo', {},
env("foo" => Sass::Script::String.new("foo"))))
@@ -424,6 +473,15 @@ def test_empty_list
assert_raise_message(Sass::SyntaxError, "() isn't a valid CSS value.") {resolve("nth(append((), ()), 1)")}
end
+ def test_list_with_nulls
+ assert_equal "1, 2, 3", resolve("1, 2, null, 3")
+ assert_equal "1 2 3", resolve("1 2 null 3")
+ assert_equal "1, 2, 3", resolve("1, 2, 3, null")
+ assert_equal "1 2 3", resolve("1 2 3 null")
+ assert_equal "1, 2, 3", resolve("null, 1, 2, 3")
+ assert_equal "1 2 3", resolve("null 1 2 3")
+ end
+
def test_deep_argument_error_not_unwrapped
assert_raise_message(ArgumentError, 'wrong number of arguments (0 for 1)') {resolve("arg-error()")}
end