Skip to content

Commit

Permalink
Merge branch 'null-literal'
Browse files Browse the repository at this point in the history
Closes #374
  • Loading branch information
nex3 committed May 11, 2012
2 parents d7d85b9 + 76fef3a commit 709c5a9
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 14 deletions.
36 changes: 33 additions & 3 deletions doc-src/SASS_REFERENCE.md
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion lib/sass/engine.rb
Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion lib/sass/script/lexer.rb
Expand Up @@ -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)})},
}
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions lib/sass/script/list.rb
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lib/sass/script/literal.rb
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions lib/sass/script/null.rb
@@ -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
5 changes: 5 additions & 0 deletions lib/sass/script/operation.rb
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/sass/script/parser.rb
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion lib/sass/scss/rx.rb
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions lib/sass/tree/prop_node.rb
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion lib/sass/tree/visitors/perform.rb
Expand Up @@ -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)
[]
Expand Down
1 change: 1 addition & 0 deletions lib/sass/tree/visitors/to_css.rb
Expand Up @@ -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}"
Expand Down
61 changes: 60 additions & 1 deletion test/sass/engine_test.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -1393,6 +1423,15 @@ def test_complex_property_interpolation
def test_if_directive
assert_equal("a {\n b: 1; }\n", render(<<SASS))
$var: true
a
@if $var
b: 1
@if not $var
b: 2
SASS

assert_equal("a {\n b: 2; }\n", render(<<SASS))
$var: null
a
@if $var
b: 1
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions test/sass/functions_test.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 709c5a9

Please sign in to comment.