From 917bd658c5e9f6c8e2d3e8e7e7cdab45919b3856 Mon Sep 17 00:00:00 2001 From: Garry Hill Date: Wed, 9 Apr 2008 10:21:49 +0100 Subject: [PATCH] Mixin patch for Sass. --- README.rdoc | 34 +++++++++++++ lib/sass.rb | 76 ++++++++++++++++++++++++++++ lib/sass/engine.rb | 71 ++++++++++++++++++++++---- test/sass/engine_test.rb | 7 +++ test/sass/results/mixins.css | 88 +++++++++++++++++++++++++++++++++ test/sass/templates/mixins.sass | 71 ++++++++++++++++++++++++++ 6 files changed, 337 insertions(+), 10 deletions(-) create mode 100644 test/sass/results/mixins.css create mode 100644 test/sass/templates/mixins.sass diff --git a/README.rdoc b/README.rdoc index 49f089fc..403d491a 100644 --- a/README.rdoc +++ b/README.rdoc @@ -228,6 +228,40 @@ becomes: background-color: #79d645; width: 15em; } +Taking the idea of constants a bit further are mixins. +These let you group whole swathes of CSS attributes into a single +directive and then include those anywhere you want: + +-blue-border + :border + :color blue + :width 2px + :style dotted + +.comment + +blue-border + :padding 2px + :margin 10px 0 + +.reply + +blue-border + +becomes: + +.comment { + border-color: blue; + border-width: 2px; + border-style: dotted; + padding: 2px; + margin: 10px 0; +} + +.reply { + border-color: blue; + border-width: 2px; + border-style: dotted; +} + A comprehensive list of features is in the documentation for the Sass module. diff --git a/lib/sass.rb b/lib/sass.rb index 3b63ab94..ed73c152 100644 --- a/lib/sass.rb +++ b/lib/sass.rb @@ -595,6 +595,82 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) # background-image: url(/images/pbj.png); # color: red; } # +# == Mixins +# +# Mixins enable you to define groups of CSS attributes and +# then include them inline in any number of selectors +# throughout the document. +# +# === Defining a Mixin +# +# To define a mixin you use a slightly modified form of selector syntax. +# For example the 'large-text' mixin is defined as follows: +# +# -large-text +# :font +# :family Arial +# :size 20px +# :weight bold +# :color #ff0000 +# +# Anything you can put into a standard selector, +# you can put into a mixin definition. e.g. +# +# -clearfix +# display: inline-block +# &:after +# content: "." +# display: block +# height: 0 +# clear: both +# visibility: hidden +# * html & +# height: 1px +# +# +# === Mixing it in +# +# Inlining a defined mixin is simple, +# just prepend a '+' symbol to the name of a mixin defined earlier in the document. +# So to inline the 'large-text' defined earlier, +# we include the statment '+large-text' in our selector definition thus: +# +# .page-title +# +large-text +# :padding 4px +# :margin +# :top 10px +# +# +# This will produce the following CSS output: +# +# .page-title { +# font-family: Arial; +# font-size: 20px; +# font-weight: bold; +# color: #ff0000; +# padding: 4px; +# margin-top: 10px; +# } +# +# Any number of mixins may be defined and there is no limit on +# the number that can be included in a particular selector. +# +# Mixin definitions can also include references to other mixins defined earlier in the file. +# E.g. +# +# -highlighted-background +# background: +# color: #fc0 +# -header-text +# font: +# size: 20px +# +# -compound +# +highlighted-background +# +header-text +# +# # == Output Style # # Although the default CSS style that Sass outputs is very nice, diff --git a/lib/sass/engine.rb b/lib/sass/engine.rb index 489c6ce0..fd0c2b63 100755 --- a/lib/sass/engine.rb +++ b/lib/sass/engine.rb @@ -43,6 +43,12 @@ module Sass # Designates a non-parsed rule. ESCAPE_CHAR = ?\\ + # Designates block as mixin definition rather than CSS rules to output + MIXIN_DEFINITION_CHAR = ?- + + # Includes named mixin declared using MIXIN_DEFINITION_CHAR + MIXIN_INCLUDE_CHAR = ?+ + # The regex that matches and extracts data from # attributes of the form :name attr. ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/ @@ -74,6 +80,7 @@ module Sass @template = template.split(/\n?\r|\r?\n/) @lines = [] @constants = {"important" => "!important"} + @mixins = {} end # Processes the template and returns the result as a string. @@ -179,10 +186,16 @@ module Sass if node == :constant raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line) elsif node.is_a? Array - raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line) + # arrays can either be full of import statements + # or attributes from mixin includes + # in either case they shouldn't have children. + # Need to peek into the array in order to give meaningful errors + directive_type = (node.first.is_a?(Tree::DirectiveNode) ? "import" : "mixin") + raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath #{directive_type} directives.", @line) end end + index = @line if node == :mixin return node, index end @@ -215,14 +228,7 @@ module Sass while has_children child, index = build_tree(index) - if child == :constant - raise SyntaxError.new("Constants may only be declared at the root of a document.", @line) - elsif child.is_a? Array - raise SyntaxError.new("Import directives may only be used at the root of a document.", @line) - elsif child.is_a? Tree::Node - child.line = @line - node << child - end + validate_and_append_child(node, child) has_children = has_children?(index, tabs) end @@ -230,6 +236,26 @@ module Sass return node, index end + def validate_and_append_child(parent, child) + case child + when :constant + raise SyntaxError.new("Constants may only be declared at the root of a document.", @line) + when :mixin + raise SyntaxError.new("Mixins may only be defined at the root of a document.", @line) + when Array + child.each do |c| + if c.is_a?(Tree::DirectiveNode) + raise SyntaxError.new("Import directives may only be used at the root of a document.", @line) + end + c.line = @line + parent << c + end + when Tree::Node + child.line = @line + parent << child + end + end + def has_children?(index, tabs) next_line = ['//', 0] while !next_line.nil? && next_line[0] == '//' && next_line[1] = 0 @@ -255,6 +281,10 @@ module Sass parse_directive(line) when ESCAPE_CHAR Tree::RuleNode.new(line[1..-1], @options[:style]) + when MIXIN_DEFINITION_CHAR + parse_mixin_definition(line) + when MIXIN_INCLUDE_CHAR + parse_mixin_include(line) else if line =~ ATTRIBUTE_ALTERNATE_MATCHER parse_attribute(line, ATTRIBUTE_ALTERNATE) @@ -324,6 +354,27 @@ module Sass end end + def parse_mixin_definition(line) + mixin_name = line[1..-1] + @mixins[mixin_name] = [] + index = @line + line, tabs = @lines[index] + while !line.nil? && tabs > 0 + child, index = build_tree(index) + validate_and_append_child(@mixins[mixin_name], child) + line, tabs = @lines[index] + end + :mixin + end + + def parse_mixin_include(line) + mixin_name = line[1..-1] + unless @mixins.has_key?(mixin_name) + raise SyntaxError.new("Undefined mixin '#{mixin_name}'", @line) + end + @mixins[mixin_name] + end + def import(files) nodes = [] @@ -337,7 +388,7 @@ module Sass end if filename =~ /\.css$/ - nodes << Tree::ValueNode.new("@import url(#{filename});", @options[:style]) + nodes << Tree::DirectiveNode.new("@import url(#{filename})", @options[:style]) else File.open(filename) do |file| new_options = @options.dup diff --git a/test/sass/engine_test.rb b/test/sass/engine_test.rb index e5adcc1a..52ec96b7 100755 --- a/test/sass/engine_test.rb +++ b/test/sass/engine_test.rb @@ -48,6 +48,9 @@ class SassEngineTest < Test::Unit::TestCase "foo\n @import templates/basic" => "Import directives may only be used at the root of a document.", "!foo = bar baz !" => "Unterminated constant.", "!foo = !(foo)" => "Invalid constant.", + "-foo\n :color red\n.bar\n +bang" => "Undefined mixin 'bang'", + ".bar\n -foo\n :color red\n" => "Mixins may only be defined at the root of a document.", + "-foo\n :color red\n.bar\n +foo\n :color red" => "Illegal nesting: Nothing may be nested beneath mixin directives.", } def test_basic_render @@ -232,6 +235,10 @@ END assert_equal("foo {\n a: b; }\n", render("!foo ||= b\nfoo\n a = !foo")) end + def test_mixins + renders_correctly "mixins", { :style => :expanded } + end + private def render(sass, options = {}) diff --git a/test/sass/results/mixins.css b/test/sass/results/mixins.css new file mode 100644 index 00000000..542c1423 --- /dev/null +++ b/test/sass/results/mixins.css @@ -0,0 +1,88 @@ +#main { + width: 15em; + color: #0000ff; +} +#main p { + border-top-width: 2px; + border-top-color: #ffcc00; + border-left-width: 1px; + border-left-color: #000; + border-style: dotted; + border-width: 2px; +} +#main .cool { + width: 100px; +} + +#left { + border-top-width: 2px; + border-top-color: #ffcc00; + border-left-width: 1px; + border-left-color: #000; + font-size: 2em; + font-weight: bold; + float: left; +} + +#right { + border-top-width: 2px; + border-top-color: #ffcc00; + border-left-width: 1px; + border-left-color: #000; + color: #f00; + font-size: 20px; + float: right; +} + +.bordered { + border-top-width: 2px; + border-top-color: #ffcc00; + border-left-width: 1px; + border-left-color: #000; +} + +.complex { + color: #f00; + font-size: 20px; + text-decoration: none; +} +.complex:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} +* html .complex { + height: 1px; + color: #f00; + font-size: 20px; +} + +.more-complex { + color: #f00; + font-size: 20px; + text-decoration: none; + display: inline; +} +.more-complex:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} +* html .more-complex { + height: 1px; + color: #f00; + font-size: 20px; +} +.more-complex a:hover { + text-decoration: underline; + color: #f00; + font-size: 20px; + border-top-width: 2px; + border-top-color: #ffcc00; + border-left-width: 1px; + border-left-color: #000; +} diff --git a/test/sass/templates/mixins.sass b/test/sass/templates/mixins.sass new file mode 100644 index 00000000..91230be9 --- /dev/null +++ b/test/sass/templates/mixins.sass @@ -0,0 +1,71 @@ +!yellow = #fc0 + +-bordered + :border + :top + :width 2px + :color = !yellow + :left + :width 1px + :color #000 +-header-font + :color #f00 + :font + :size 20px + +-compound + +header-font + +bordered + +-complex + +header-font + text: + decoration: none + &:after + content: "." + display: block + height: 0 + clear: both + visibility: hidden + * html & + height: 1px + +header-font +-deep + a:hover + :text-decoration underline + +compound + + +#main + :width 15em + :color #0000ff + p + +bordered + :border + :style dotted + :width 2px + .cool + :width 100px + +#left + +bordered + :font + :size 2em + :weight bold + :float left + +#right + +bordered + +header-font + :float right + +.bordered + +bordered + +.complex + +complex + +.more-complex + +complex + +deep + display: inline