Permalink
Browse files

Merge branch 'null-literal'

Closes #374
  • Loading branch information...
2 parents d7d85b9 + 76fef3a commit 709c5a96358d212be5d85ac15c99304fcd67d24a @nex3 nex3 committed May 11, 2012
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 (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,
@@ -708,8 +709,10 @@ Lists can also have no items in them at all.
These lists are represented as `()`.
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.
+If a list contains empty lists or null values,
+as in `1px 2px () 3px` or `1px 2px null 3px`,
+the empty lists and null values will be removed
+before the containing list is turned into CSS.
### Operations
@@ -932,6 +935,19 @@ is compiled to:
p:before {
content: "I ate 15 pies!"; }
+Null values are treated as empty strings for string operations and 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 +1062,20 @@ is compiled to:
content: "First content";
new-content: "First time reference"; }
+Variables with `null` values are treated as unassigned by !default:
+
+ $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
@@ -600,7 +600,14 @@ def parse_property(name, parsed_name, value, prop, line)
else
expr = parse_script(value, :offset => line.offset + line.text.index(value))
end
- Tree::PropNode.new(parse_interp(name), expr, prop)
+ node = Tree::PropNode.new(parse_interp(name), expr, prop)
+ if value.strip.empty? && line.children.empty?
+ raise SyntaxError.new(
+ "Invalid property: \"#{node.declaration}\" (no value)." +
+ node.pseudo_class_selector_message)
+ end
+
+ node
end
def parse_variable(line)
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.
@@ -217,6 +218,13 @@ def to_s(opts = {})
end
alias_method :to_sass, :to_s
+ # Returns whether or not this object is null.
+ #
+ # @return [Boolean] `false`
+ def null?
+ false
+ end
+
protected
# Evaluates 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 null?
+ 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
@@ -455,7 +455,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,
View
@@ -130,7 +130,7 @@ def self.quote(str, flags = 0)
# about 50 characters. This mitigates the problem of exponential parsing
# time when a value has a long string of valid, parsable content followed
# by something invalid.
- STATIC_VALUE = /(-?#{NMSTART}|#{STRING_NOINTERP}|[ \t](?!%)|#[a-f0-9]|[,%]|#{NUM}|\!important){0,50}([;}])/i
+ STATIC_VALUE = /(-?#{NMSTART}|#{STRING_NOINTERP}|[ \t](?!%)|#[a-f0-9]|[,%]|#{NUM}|\!important){1,50}([;}])/i
STATIC_SELECTOR = /(#{NMCHAR}|[ \t]|[,>+*]|[:#.]#{NMSTART}){0,50}([{])/i
end
end
View
@@ -92,15 +92,19 @@ def declaration(opts = {:old => @prop_syntax == :old}, fmt = :sass)
"#{initial}#{name}#{mid} #{self.class.val_to_sass(value, opts)}".rstrip
end
+ # A property node is invisible if its value is empty.
+ #
+ # @return [Boolean]
+ def invisible?
+ resolved_value.empty?
+ end
+
private
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?
- raise Sass::SyntaxError.new("Invalid property: #{declaration.dump} (no value)." +
- pseudo_class_selector_message)
end
end
@@ -261,7 +261,8 @@ def visit_rule(node)
# Loads the new variable value into the environment.
def visit_variable(node)
- return [] if node.guarded && !@environment.var(node.name).nil?
+ var = @environment.var(node.name)
+ return [] if node.guarded && var && !var.null?
val = node.expr.perform(@environment)
@environment.set_var(node.name, val)
[]
@@ -122,6 +122,7 @@ def visit_cssimport(node)
end
def visit_prop(node)
+ return if node.resolved_value.empty?
tab_str = ' ' * (@tabs + node.tabs)
if node.style == :compressed
"#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
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
@@ -1186,6 +1187,35 @@ def test_default_values_for_mixin_arguments
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
def test_hyphen_underscore_insensitive_mixins
@@ -1399,6 +1429,15 @@ def test_if_directive
@if not $var
b: 2
SASS
+
+ assert_equal("a {\n b: 2; }\n", render(<<SASS))
+$var: null
+a
+ @if $var
+ b: 1
+ @if not $var
+ b: 2
+SASS
end
def test_for
@@ -1795,6 +1834,26 @@ def test_empty_selector_warning
END
end
+ def test_nonprinting_empty_property
+ assert_equal(<<CSS, render(<<SASS))
+a {
+ c: "";
+ e: f; }
+CSS
+$null-value: null
+$empty-string: ''
+$empty-list: (null)
+a
+ b: $null-value
+ c: $empty-string
+ d: $empty-list
+ e: f
+
+g
+ h: null
+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
Oops, something went wrong.

0 comments on commit 709c5a9

Please sign in to comment.