├── lib ├── csspool │ ├── sac.rb │ ├── terms │ │ ├── uri.rb │ │ ├── hash.rb │ │ ├── string.rb │ │ ├── ident.rb │ │ ├── number.rb │ │ ├── function.rb │ │ └── rgb.rb │ ├── selectors │ │ ├── additional.rb │ │ ├── type.rb │ │ ├── universal.rb │ │ ├── id.rb │ │ ├── class.rb │ │ ├── pseudo_class.rb │ │ ├── attribute.rb │ │ └── simple.rb │ ├── visitors.rb │ ├── terms.rb │ ├── css.rb │ ├── css │ │ ├── media.rb │ │ ├── charset.rb │ │ ├── rule_set.rb │ │ ├── declaration.rb │ │ ├── import_rule.rb │ │ ├── document.rb │ │ ├── document_handler.rb │ │ ├── tokenizer.rex │ │ └── parser.y │ ├── selectors.rb │ ├── sac │ │ ├── parser.rb │ │ └── document.rb │ ├── visitors │ │ ├── visitor.rb │ │ ├── children.rb │ │ ├── iterator.rb │ │ ├── comparable.rb │ │ └── to_css.rb │ ├── selector.rb │ ├── node.rb │ └── collection.rb └── csspool.rb ├── .gitignore ├── test ├── files │ └── test.css ├── visitors │ ├── test_children.rb │ ├── test_each.rb │ ├── test_comparable.rb │ └── test_to_css.rb ├── css │ ├── test_document.rb │ ├── test_import_rule.rb │ ├── test_tokenizer.rb │ └── test_parser.rb ├── test_selector.rb ├── sac │ ├── test_properties.rb │ ├── test_parser.rb │ └── test_terms.rb ├── helper.rb ├── test_collection.rb └── test_parser.rb ├── .autotest ├── Gemfile ├── Rakefile ├── Manifest.txt ├── README.rdoc └── CHANGELOG.rdoc /lib/csspool/sac.rb: -------------------------------------------------------------------------------- 1 | require 'csspool/sac/document' 2 | require 'csspool/sac/parser' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc 2 | /pkg 3 | 4 | /lib/csspool/css/tokenizer.rb 5 | /lib/csspool/css/parser.rb 6 | /Gemfile.lock 7 | -------------------------------------------------------------------------------- /lib/csspool/terms/uri.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Terms 3 | class URI < Ident 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/files/test.css: -------------------------------------------------------------------------------- 1 | div > p { 2 | background-color: #f00; 3 | } 4 | 5 | .test { 6 | box-shadow: 20px; 7 | } 8 | -------------------------------------------------------------------------------- /lib/csspool/terms/hash.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Terms 3 | class Hash < Ident 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/csspool/terms/string.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Terms 3 | class String < CSSPool::Terms::Ident 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/csspool/selectors/additional.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Selectors 3 | class Additional < CSSPool::Node 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/csspool/selectors/type.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Selectors 3 | class Type < CSSPool::Selectors::Simple 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/csspool/selectors/universal.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Selectors 3 | class Universal < CSSPool::Selectors::Simple 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/csspool/visitors.rb: -------------------------------------------------------------------------------- 1 | require 'csspool/visitors/visitor' 2 | require 'csspool/visitors/to_css' 3 | require 'csspool/visitors/comparable' 4 | require 'csspool/visitors/iterator' 5 | require 'csspool/visitors/children' 6 | -------------------------------------------------------------------------------- /lib/csspool/terms.rb: -------------------------------------------------------------------------------- 1 | require 'csspool/terms/ident' 2 | require 'csspool/terms/string' 3 | require 'csspool/terms/number' 4 | require 'csspool/terms/function' 5 | require 'csspool/terms/uri' 6 | require 'csspool/terms/rgb' 7 | require 'csspool/terms/hash' 8 | -------------------------------------------------------------------------------- /lib/csspool/selectors/id.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Selectors 3 | class Id < CSSPool::Selectors::Additional 4 | attr_accessor :name 5 | 6 | def initialize name 7 | @name = name 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/csspool/selectors/class.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Selectors 3 | class Class < CSSPool::Selectors::Additional 4 | attr_accessor :name 5 | 6 | def initialize name 7 | @name = name 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/csspool/css.rb: -------------------------------------------------------------------------------- 1 | require 'csspool/css/tokenizer' 2 | require 'csspool/css/charset' 3 | require 'csspool/css/import_rule' 4 | require 'csspool/css/media' 5 | require 'csspool/css/rule_set' 6 | require 'csspool/css/declaration' 7 | require 'csspool/css/document' 8 | require 'csspool/css/document_handler' 9 | -------------------------------------------------------------------------------- /lib/csspool/css/media.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module CSS 3 | class Media < CSSPool::Node 4 | attr_accessor :name 5 | attr_accessor :parse_location 6 | 7 | def initialize name, parse_location 8 | @name = name 9 | @parse_location = parse_location 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/csspool/selectors.rb: -------------------------------------------------------------------------------- 1 | require 'csspool/selectors/simple' 2 | require 'csspool/selectors/universal' 3 | require 'csspool/selectors/type' 4 | require 'csspool/selectors/additional' 5 | require 'csspool/selectors/id' 6 | require 'csspool/selectors/class' 7 | require 'csspool/selectors/pseudo_class' 8 | require 'csspool/selectors/attribute' 9 | -------------------------------------------------------------------------------- /.autotest: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | begin 4 | require 'autotest/fsevent' 5 | rescue LoadError 6 | end 7 | 8 | Autotest.add_hook :run_command do |at| 9 | at.unit_diff = 'cat' 10 | end 11 | 12 | Autotest.add_hook :ran_command do |at| 13 | File.open('/tmp/autotest.txt', 'wb') { |f| 14 | f.write(at.results.join) 15 | } 16 | end 17 | -------------------------------------------------------------------------------- /lib/csspool/css/charset.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module CSS 3 | class Charset < CSSPool::Node 4 | attr_accessor :name 5 | attr_accessor :parse_location 6 | 7 | def initialize name, parse_location 8 | @name = name 9 | @parse_location = parse_location 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/csspool/selectors/pseudo_class.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Selectors 3 | class PseudoClass < CSSPool::Selectors::Additional 4 | attr_accessor :name 5 | attr_accessor :extra 6 | 7 | def initialize name, extra = nil 8 | @name = name 9 | @extra = extra 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/csspool/sac/parser.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module SAC 3 | class Parser < CSSPool::CSS::Tokenizer 4 | attr_accessor :handler 5 | 6 | def initialize handler = CSSPool::CSS::DocumentHandler.new 7 | @handler = handler 8 | end 9 | 10 | def parse string 11 | scan_str string 12 | @handler.document 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/csspool/terms/ident.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Terms 3 | class Ident < CSSPool::Node 4 | attr_accessor :value 5 | attr_accessor :operator 6 | attr_accessor :parse_location 7 | 8 | def initialize value, operator = nil, parse_location = {} 9 | @value = value 10 | @operator = operator 11 | @parse_location = parse_location 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/csspool/terms/number.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Terms 3 | class Number < Ident 4 | attr_accessor :type 5 | attr_accessor :unary_operator 6 | 7 | def initialize value, unary_operator = nil, type = nil, operator = nil, parse_location = {} 8 | @type = type 9 | @unary_operator = unary_operator 10 | super(value, operator, parse_location) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/csspool/css/rule_set.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module CSS 3 | class RuleSet < CSSPool::Node 4 | attr_accessor :selectors 5 | attr_accessor :declarations 6 | attr_accessor :media 7 | 8 | def initialize selectors, declarations = [], media = [] 9 | @selectors = selectors 10 | @declarations = declarations 11 | @media = media 12 | 13 | selectors.each { |sel| sel.rule_set = self } 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/csspool.rb: -------------------------------------------------------------------------------- 1 | require 'csspool/node' 2 | require 'csspool/selectors' 3 | require 'csspool/terms' 4 | require 'csspool/selector' 5 | old = $-w 6 | $-w = false 7 | require 'csspool/css/parser' 8 | $-w = old 9 | require 'csspool/css/tokenizer' 10 | require 'csspool/sac' 11 | require 'csspool/css' 12 | require 'csspool/visitors' 13 | require 'csspool/collection' 14 | 15 | module CSSPool 16 | VERSION = "3.0.2" 17 | 18 | def self.CSS doc 19 | CSSPool::CSS::Document.parse doc 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/csspool/terms/function.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Terms 3 | class Function < CSSPool::Node 4 | attr_accessor :name 5 | attr_accessor :params 6 | attr_accessor :parse_location 7 | attr_accessor :operator 8 | 9 | def initialize name, params, operator = nil, parse_location = nil 10 | @name = name 11 | @params = params 12 | @operator = operator 13 | @parse_location = parse_location 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/csspool/visitors/visitor.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Visitors 3 | class Visitor 4 | def self.visitor_for *klasses, &block 5 | klasses.each do |klass| 6 | method_name = klass.name.split('::').join('_') 7 | define_method(:"visit_#{method_name}", block) 8 | end 9 | end 10 | 11 | def accept target 12 | method_name = target.class.name.split('::').join('_') 13 | send(:"visit_#{method_name}", target) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/csspool/selectors/attribute.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Selectors 3 | class Attribute < CSSPool::Selectors::Additional 4 | attr_accessor :name 5 | attr_accessor :value 6 | attr_accessor :match_way 7 | 8 | NO_MATCH = 0 9 | SET = 1 10 | EQUALS = 2 11 | INCLUDES = 3 12 | DASHMATCH = 4 13 | 14 | def initialize name, value, match_way 15 | @name = name 16 | @value = value 17 | @match_way = match_way 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/visitors/test_children.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | module Visitors 5 | class TestChildren < CSSPool::TestCase 6 | def test_iterate 7 | doc = CSSPool.CSS <<-eocss 8 | @charset "UTF-8"; 9 | @import url("foo.css") screen; 10 | div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 11 | eocss 12 | 13 | stack = [doc] 14 | until stack.empty? do 15 | stack += stack.pop.children 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/csspool/css/declaration.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module CSS 3 | class Declaration < CSSPool::Node 4 | attr_accessor :property 5 | attr_accessor :expressions 6 | attr_accessor :important 7 | attr_accessor :rule_set 8 | 9 | alias :important? :important 10 | 11 | def initialize property, expressions, important, rule_set 12 | @property = property 13 | @expressions = expressions 14 | @important = important 15 | @rule_set = rule_set 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/visitors/test_each.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | module Visitors 5 | class TestEach < CSSPool::TestCase 6 | def test_iterate 7 | doc = CSSPool.CSS <<-eocss 8 | @charset "UTF-8"; 9 | @import url("foo.css") screen; 10 | div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 11 | eocss 12 | list = [] 13 | doc.each { |node| list << node } 14 | assert_equal 20, list.length 15 | assert list.hash 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/csspool/terms/rgb.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Terms 3 | class Rgb < CSSPool::Node 4 | attr_accessor :red 5 | attr_accessor :green 6 | attr_accessor :blue 7 | attr_accessor :parse_location 8 | attr_accessor :operator 9 | 10 | def initialize red, green, blue, operator = nil, parse_location = {} 11 | super() 12 | @red = red 13 | @green = green 14 | @blue = blue 15 | @operator = operator 16 | @parse_location = parse_location 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | # DO NOT EDIT THIS FILE. Instead, edit Rakefile, and run `rake bundler:gemfile`. 4 | 5 | source :gemcutter 6 | 7 | 8 | gem "rdoc", "~>3.10", :group => [:development, :test] 9 | gem "racc", ">=0", :group => [:development, :test] 10 | gem "rexical", ">=0", :group => [:development, :test] 11 | gem "hoe-git", ">=0", :group => [:development, :test] 12 | gem "hoe-gemspec", ">=0", :group => [:development, :test] 13 | gem "hoe-bundler", ">=0", :group => [:development, :test] 14 | gem "hoe", "~>3.0", :group => [:development, :test] 15 | 16 | # vim: syntax=ruby 17 | -------------------------------------------------------------------------------- /test/css/test_document.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | module CSSPool 3 | module CSS 4 | class TestDocument < CSSPool::TestCase 5 | def test_file_open 6 | doc = File.open("#{ASSET_DIR}/test.css") do |f| 7 | CSSPool.CSS f 8 | end 9 | 10 | assert_equal 1, doc['.test'].length 11 | end 12 | 13 | def test_search 14 | doc = CSSPool.CSS("div > p { background: red; }\n") 15 | assert_equal 1, doc['div > p'].length 16 | assert_equal 1, doc['div > p'].first.declarations.length 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/csspool/selectors/simple.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Selectors 3 | class Simple < CSSPool::Node 4 | NO_COMBINATOR = 0 5 | DESCENDENT = 1 6 | PRECEDED_BY = 2 7 | CHILD = 3 8 | 9 | attr_accessor :name 10 | attr_accessor :parse_location 11 | attr_accessor :additional_selectors 12 | attr_accessor :combinator 13 | 14 | def initialize name, combinator = nil 15 | @name = name 16 | @combinator = combinator 17 | @parse_location = nil 18 | @additional_selectors = [] 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/csspool/sac/document.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module SAC 3 | class Document 4 | def start_document 5 | end 6 | 7 | def end_document 8 | end 9 | 10 | def charset name, location 11 | end 12 | 13 | def import_style media_list, uri, default_ns = nil, location = {} 14 | end 15 | 16 | def start_selector selector_list 17 | end 18 | 19 | def end_selector selector_list 20 | end 21 | 22 | def property name, expression, important 23 | end 24 | 25 | def comment comment 26 | end 27 | 28 | def start_media media_list, parse_location = {} 29 | end 30 | 31 | def end_media media_list, parse_location = {} 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/csspool/css/import_rule.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module CSS 3 | class ImportRule < CSSPool::Node 4 | attr_accessor :uri 5 | attr_accessor :namespace 6 | attr_accessor :media 7 | attr_accessor :document 8 | attr_accessor :parse_location 9 | 10 | def initialize uri, namespace, media, document, parse_location 11 | @uri = uri 12 | @namespace = namespace 13 | @media = media 14 | @document = document 15 | @parse_location = parse_location 16 | end 17 | 18 | def load 19 | new_doc = CSSPool.CSS(yield uri.value) 20 | new_doc.parent_import_rule = self 21 | new_doc.parent = document 22 | new_doc.rule_sets.each { |rs| rs.media = media } 23 | new_doc 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/csspool/selector.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | class Selector < CSSPool::Node 3 | attr_accessor :simple_selectors 4 | attr_accessor :parse_location 5 | attr_accessor :rule_set 6 | 7 | def initialize simple_selectors = [], parse_location = {} 8 | @simple_selectors = simple_selectors 9 | @parse_location = parse_location 10 | @rule_set = nil 11 | end 12 | 13 | def declarations 14 | @rule_set.declarations 15 | end 16 | 17 | def specificity 18 | a = b = c = 0 19 | simple_selectors.each do |s| 20 | c += 1 21 | s.additional_selectors.each do |additional_selector| 22 | if Selectors::Id === additional_selector 23 | a += 1 24 | else 25 | b += 1 26 | end 27 | end 28 | end 29 | [a, b, c] 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/csspool/node.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | class Node 3 | include Enumerable 4 | 5 | def accept target 6 | target.accept self 7 | end 8 | 9 | def to_css options={} 10 | if options[:minify] 11 | to_minified_css 12 | else 13 | accept Visitors::ToCSS.new 14 | end 15 | end 16 | alias :to_s :to_css 17 | 18 | def to_minified_css 19 | accept Visitors::ToMinifiedCSS.new 20 | end 21 | 22 | def == other 23 | return false unless self.class == other.class 24 | 25 | accept Visitors::Comparable.new other 26 | end 27 | alias :eql? :== 28 | 29 | def each &block 30 | Visitors::Iterator.new(block).accept self 31 | end 32 | 33 | def children 34 | accept Visitors::Children.new 35 | end 36 | 37 | def hash 38 | @hash ||= children.map { |child| child.hash }.hash 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/csspool/collection.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | class Collection 3 | include Enumerable 4 | 5 | def initialize &block 6 | @docs = [] 7 | @block = block 8 | end 9 | 10 | def << string 11 | doc = CSSPool.CSS string 12 | 13 | import_tree = [[doc]] 14 | 15 | imported_urls = {} 16 | 17 | until import_tree.last.all? { |x| x.import_rules.length == 0 } 18 | level = import_tree.last 19 | import_tree << [] 20 | level.each do |l| 21 | l.import_rules.each do |ir| 22 | next if imported_urls.key? ir.uri 23 | 24 | new_doc = ir.load(&@block) 25 | 26 | imported_urls[ir.uri] = ir.load(&@block) 27 | import_tree.last << new_doc 28 | end 29 | end 30 | end 31 | 32 | @docs += import_tree.flatten.reverse 33 | self 34 | end 35 | 36 | def length 37 | @docs.length 38 | end 39 | 40 | def [] idx 41 | @docs[idx] 42 | end 43 | 44 | def each &block 45 | @docs.each(&block) 46 | end 47 | 48 | def last; @docs.last; end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/csspool/css/document.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module CSS 3 | class Document < Node 4 | def self.parse string 5 | # If a File object gets passed in, via functions like File.open 6 | # or Kernel::open 7 | if string.respond_to? :read 8 | string = string.read 9 | end 10 | 11 | unless string && string.length > 0 12 | return CSSPool::CSS::Document.new 13 | end 14 | 15 | handler = CSSPool::CSS::DocumentHandler.new 16 | parser = CSSPool::SAC::Parser.new(handler) 17 | parser.parse(string) 18 | handler.document 19 | end 20 | 21 | attr_accessor :rule_sets 22 | attr_accessor :charsets 23 | attr_accessor :import_rules 24 | attr_accessor :parent 25 | attr_accessor :parent_import_rule 26 | 27 | def initialize 28 | @rule_sets = [] 29 | @charsets = [] 30 | @import_rules = [] 31 | @parent = nil 32 | @parent_import_rule = nil 33 | end 34 | 35 | def [] selector 36 | selectors = CSSPool.CSS("#{selector} {}").rule_sets.first.selectors 37 | rule_sets.find_all { |rs| rs.selectors == selectors} 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/css/test_import_rule.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | module CSS 5 | class TestImportRule < CSSPool::TestCase 6 | def test_import 7 | doc = CSSPool.CSS <<-eocss 8 | @import "foo.css"; 9 | eocss 10 | 11 | assert_equal 1, doc.import_rules.length 12 | 13 | doc.import_rules.each do |ir| 14 | new_doc = ir.load do |url| 15 | assert_equal "foo.css", url 16 | "div { background: red; }" 17 | end 18 | assert new_doc 19 | assert_equal 1, new_doc.rule_sets.length 20 | assert_equal ir, new_doc.parent_import_rule 21 | assert_equal doc, new_doc.parent 22 | end 23 | end 24 | 25 | def test_import_with_media 26 | doc = CSSPool.CSS <<-eocss 27 | @import "foo.css" screen, print; 28 | eocss 29 | 30 | assert_equal 1, doc.import_rules.length 31 | doc.import_rules.each do |ir| 32 | new_doc = ir.load do |url| 33 | "div { background: red; }" 34 | end 35 | new_doc.rule_sets.each do |rs| 36 | assert_equal ir.media, rs.media 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/csspool/visitors/children.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Visitors 3 | class Children < Visitor 4 | visitor_for CSS::Document do |target| 5 | children = [] 6 | [:charsets, :import_rules, :rule_sets].each do |member| 7 | children += target.send(member) 8 | end 9 | children 10 | end 11 | 12 | visitor_for CSS::ImportRule do |target| 13 | target.media 14 | end 15 | 16 | visitor_for CSS::Media, 17 | CSS::Charset, 18 | Selectors::Id, 19 | Selectors::Class, 20 | Selectors::PseudoClass, 21 | Selectors::Attribute, 22 | Terms::Ident, 23 | Terms::String, 24 | Terms::URI, 25 | Terms::Number, 26 | Terms::Hash, 27 | Terms::Function, 28 | Terms::Rgb do |target| 29 | [] 30 | end 31 | 32 | visitor_for CSS::Declaration do |target| 33 | target.expressions 34 | end 35 | 36 | visitor_for CSS::RuleSet do |target| 37 | target.selectors + target.declarations 38 | end 39 | 40 | visitor_for Selector do |target| 41 | target.simple_selectors 42 | end 43 | 44 | visitor_for Selectors::Type, Selectors::Universal, Selectors::Simple do |target| 45 | target.additional_selectors 46 | end 47 | end 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /test/test_selector.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | class TestSelector < CSSPool::TestCase 5 | def test_specificity 6 | doc = CSSPool.CSS <<-eocss 7 | *, foo > bar, #hover, :hover, div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 8 | eocss 9 | selectors = doc.rule_sets.first.selectors 10 | specs = selectors.map do |sel| 11 | sel.specificity 12 | end 13 | assert_equal [[0, 0, 1], 14 | [0, 0, 2], 15 | [1, 0, 1], 16 | [0, 1, 1], 17 | [1, 0, 1], 18 | [0, 1, 1], 19 | [0, 1, 1], 20 | [0, 2, 1]], specs 21 | end 22 | 23 | def test_selector_knows_its_ruleset 24 | doc = CSSPool.CSS <<-eocss 25 | a[href][int="10"]{ background: red; } 26 | eocss 27 | rs = doc.rule_sets.first 28 | assert_equal rs, rs.selectors.first.rule_set 29 | end 30 | 31 | def test_selector_gets_declarations 32 | doc = CSSPool.CSS <<-eocss 33 | a[href][int="10"]{ background: red; } 34 | eocss 35 | rs = doc.rule_sets.first 36 | assert_equal rs.declarations, rs.selectors.first.declarations 37 | end 38 | 39 | def test_declaration_should_know_ruleset 40 | doc = CSSPool.CSS <<-eocss 41 | a[href][int="10"]{ background: red; } 42 | eocss 43 | rs = doc.rule_sets.first 44 | rs.declarations.each { |del| assert_equal rs, del.rule_set } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/csspool/css/document_handler.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module CSS 3 | class DocumentHandler < CSSPool::SAC::Document 4 | attr_accessor :document 5 | 6 | def initialize 7 | @document = nil 8 | @media_stack = [] 9 | end 10 | 11 | def start_document 12 | @document = CSSPool::CSS::Document.new 13 | end 14 | 15 | def charset name, location 16 | @document.charsets << CSS::Charset.new(name, location) 17 | end 18 | 19 | def import_style media_list, uri, ns = nil, loc = {} 20 | @document.import_rules << CSS::ImportRule.new( 21 | uri, 22 | ns, 23 | media_list.map { |x| CSS::Media.new(x, loc) }, 24 | @document, 25 | loc 26 | ) 27 | end 28 | 29 | def start_selector selector_list 30 | @document.rule_sets << RuleSet.new( 31 | selector_list, 32 | [], 33 | @media_stack.last || [] 34 | ) 35 | end 36 | 37 | def property name, exp, important 38 | rs = @document.rule_sets.last 39 | rs.declarations << Declaration.new(name, exp, important, rs) 40 | end 41 | 42 | def start_media media_list, parse_location = {} 43 | @media_stack << media_list.map { |x| CSS::Media.new(x, parse_location) } 44 | end 45 | 46 | def end_media media_list, parse_location = {} 47 | @media_stack.pop 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'rubygems' 4 | require 'hoe' 5 | 6 | GENERATED_TOKENIZER = "lib/csspool/css/tokenizer.rb" 7 | GENERATED_PARSER = "lib/csspool/css/parser.rb" 8 | 9 | Hoe.plugin :git 10 | Hoe.plugin :bundler 11 | Hoe.plugin :gemspec 12 | 13 | Hoe.spec('csspool') do 14 | developer('Aaron Patterson', 'aaronp@rubyforge.org') 15 | developer('John Barnette', 'jbarnette@rubyforge.org') 16 | self.readme_file = 'README.rdoc' 17 | self.history_file = 'CHANGELOG.rdoc' 18 | self.extra_rdoc_files = FileList['*.rdoc'] 19 | 20 | %w{racc rexical hoe-git hoe-gemspec hoe-bundler}.each do |dep| 21 | self.extra_dev_deps << [dep, '>= 0'] 22 | end 23 | end 24 | 25 | [:test, :check_manifest, "gem:spec"].each do |task_name| 26 | Rake::Task[task_name].prerequisites << :compile 27 | end 28 | 29 | task :compile => [GENERATED_TOKENIZER, GENERATED_PARSER] 30 | 31 | file GENERATED_TOKENIZER => "lib/csspool/css/tokenizer.rex" do |t| 32 | begin 33 | sh "bundle exec rex -i --independent -o #{t.name} #{t.prerequisites.first}" 34 | rescue 35 | abort "need rexical, sudo gem install rexical" 36 | end 37 | end 38 | 39 | file GENERATED_PARSER => "lib/csspool/css/parser.y" do |t| 40 | begin 41 | racc = 'bundle exec racc' 42 | #sh "#{racc} -l -o #{t.name} #{t.prerequisites.first}" 43 | sh "#{racc} -o #{t.name} #{t.prerequisites.first}" 44 | rescue 45 | abort "need racc, sudo gem install racc" 46 | end 47 | end 48 | 49 | # vim: syntax=Ruby 50 | -------------------------------------------------------------------------------- /test/sac/test_properties.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | module SAC 5 | class TestProperties < CSSPool::TestCase 6 | def setup 7 | super 8 | @doc = MyDoc.new 9 | @css = <<-eocss 10 | @charset "UTF-8"; 11 | @import url("foo.css") screen; 12 | /* This is a comment */ 13 | div a.foo, #bar, * { background: red; } 14 | div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 15 | eocss 16 | @parser = CSSPool::SAC::Parser.new(@doc) 17 | @parser.parse(@css) 18 | end 19 | 20 | def test_properties 21 | assert_equal ['background'], @doc.properties.map { |x| x.first }.uniq 22 | @doc.properties.each do |property| 23 | assert_equal 1, property[1].length 24 | end 25 | assert_equal ['red'], @doc.properties.map { |x| x[1].first.value }.uniq 26 | end 27 | 28 | def test_ident_with_comma 29 | doc = MyDoc.new 30 | parser = CSSPool::SAC::Parser.new(doc) 31 | parser.parse <<-eocss 32 | h1 { font-family: Verdana, sans-serif, monospace; } 33 | eocss 34 | assert_equal 1, doc.properties.length 35 | values = doc.properties.first[1] 36 | assert_equal 3, values.length 37 | assert_equal [nil, ',', ','], 38 | values.map { |value| value.operator } 39 | values.each { |value| assert value.parse_location } 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "csspool" 3 | 4 | module CSSPool 5 | class TestCase < Test::Unit::TestCase 6 | unless RUBY_VERSION >= '1.9' 7 | undef :default_test 8 | end 9 | 10 | ASSET_DIR = File.join(File.dirname(__FILE__), 'files') 11 | 12 | class MyDoc < CSSPool::CSS::DocumentHandler 13 | attr_accessor :start_documents, :end_documents 14 | attr_accessor :charsets, :import_styles, :comments, :start_selectors 15 | attr_accessor :end_selectors, :properties 16 | 17 | def initialize 18 | @start_documents = [] 19 | @end_documents = [] 20 | @charsets = [] 21 | @import_styles = [] 22 | @comments = [] 23 | @start_selectors = [] 24 | @end_selectors = [] 25 | @properties = [] 26 | end 27 | 28 | def property name, expression, important 29 | @properties << [name, expression] 30 | end 31 | 32 | def start_document 33 | @start_documents << true 34 | end 35 | 36 | def end_document 37 | @end_documents << true 38 | end 39 | 40 | def charset name, location 41 | @charsets << [name, location] 42 | end 43 | 44 | def import_style media_list, uri, default_ns = nil, location = {} 45 | @import_styles << [media_list, uri, default_ns, location] 46 | end 47 | 48 | def namespace_declaration prefix, uri, location 49 | @import_styles << [prefix, uri, location] 50 | end 51 | 52 | def comment string 53 | @comments << string 54 | end 55 | 56 | def start_selector selectors 57 | @start_selectors << selectors 58 | end 59 | 60 | def end_selector selectors 61 | @end_selectors << selectors 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | .autotest 2 | CHANGELOG.rdoc 3 | Gemfile 4 | Gemfile.lock 5 | Manifest.txt 6 | README.rdoc 7 | Rakefile 8 | lib/csspool.rb 9 | lib/csspool/collection.rb 10 | lib/csspool/css.rb 11 | lib/csspool/css/charset.rb 12 | lib/csspool/css/declaration.rb 13 | lib/csspool/css/document.rb 14 | lib/csspool/css/document_handler.rb 15 | lib/csspool/css/import_rule.rb 16 | lib/csspool/css/media.rb 17 | lib/csspool/css/parser.rb 18 | lib/csspool/css/parser.y 19 | lib/csspool/css/rule_set.rb 20 | lib/csspool/css/tokenizer.rb 21 | lib/csspool/css/tokenizer.rex 22 | lib/csspool/node.rb 23 | lib/csspool/sac.rb 24 | lib/csspool/sac/document.rb 25 | lib/csspool/sac/parser.rb 26 | lib/csspool/selector.rb 27 | lib/csspool/selectors.rb 28 | lib/csspool/selectors/additional.rb 29 | lib/csspool/selectors/attribute.rb 30 | lib/csspool/selectors/class.rb 31 | lib/csspool/selectors/id.rb 32 | lib/csspool/selectors/pseudo_class.rb 33 | lib/csspool/selectors/simple.rb 34 | lib/csspool/selectors/type.rb 35 | lib/csspool/selectors/universal.rb 36 | lib/csspool/terms.rb 37 | lib/csspool/terms/function.rb 38 | lib/csspool/terms/hash.rb 39 | lib/csspool/terms/ident.rb 40 | lib/csspool/terms/number.rb 41 | lib/csspool/terms/rgb.rb 42 | lib/csspool/terms/string.rb 43 | lib/csspool/terms/uri.rb 44 | lib/csspool/visitors.rb 45 | lib/csspool/visitors/children.rb 46 | lib/csspool/visitors/comparable.rb 47 | lib/csspool/visitors/iterator.rb 48 | lib/csspool/visitors/to_css.rb 49 | lib/csspool/visitors/visitor.rb 50 | test/css/test_document.rb 51 | test/css/test_import_rule.rb 52 | test/css/test_parser.rb 53 | test/css/test_tokenizer.rb 54 | test/files/test.css 55 | test/helper.rb 56 | test/sac/test_parser.rb 57 | test/sac/test_properties.rb 58 | test/sac/test_terms.rb 59 | test/test_collection.rb 60 | test/test_parser.rb 61 | test/test_selector.rb 62 | test/visitors/test_children.rb 63 | test/visitors/test_comparable.rb 64 | test/visitors/test_each.rb 65 | test/visitors/test_to_css.rb 66 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = CSSPool 2 | 3 | * http://csspool.rubyforge.org 4 | * http://github.com/tenderlove/csspool 5 | 6 | == DESCRIPTION: 7 | 8 | CSSPool is a CSS parser. CSSPool provides a SAC interface for parsing CSS as 9 | well as a document oriented interface for parsing CSS. 10 | 11 | == FEATURES/PROBLEMS: 12 | 13 | CSSPool no longer depends on libcroco! There is now a dependency on both the 14 | {racc}[http://github.com/tenderlove/racc] and 15 | {rexical}[http://github.com/tenderlove/rexical] gems, so make sure to install 16 | them first. 17 | 18 | $ sudo gem install racc 19 | $ sudo gem install rexical 20 | 21 | == SYNOPSIS: 22 | 23 | doc = CSSPool.CSS open('/path/to/css.css') 24 | doc.rule_sets.each do |rs| 25 | puts rs.to_css 26 | end 27 | 28 | puts doc.to_css 29 | 30 | == REQUIREMENTS: 31 | 32 | * racc ("sudo gem install racc") 33 | * rexical ("sudo gem install rexical") 34 | 35 | == INSTALL: 36 | 37 | * sudo gem install csspool 38 | 39 | == LICENSE: 40 | 41 | (The MIT License) 42 | 43 | Copyright (c) 2007-2013 44 | 45 | * {Aaron Patterson}[http://tenderlovemaking.com] 46 | * {John Barnette}[http://www.jbarnette.com/] 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of this software and associated documentation files (the 50 | 'Software'), to deal in the Software without restriction, including 51 | without limitation the rights to use, copy, modify, merge, publish, 52 | distribute, sublicense, and/or sell copies of the Software, and to 53 | permit persons to whom the Software is furnished to do so, subject to 54 | the following conditions: 55 | 56 | The above copyright notice and this permission notice shall be 57 | included in all copies or substantial portions of the Software. 58 | 59 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 60 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 61 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 62 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 63 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 64 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 65 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 66 | -------------------------------------------------------------------------------- /lib/csspool/visitors/iterator.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Visitors 3 | class Iterator < Visitor 4 | def initialize block 5 | @block = block 6 | end 7 | 8 | visitor_for CSS::Document do |target| 9 | [:charsets, :import_rules, :rule_sets].each do |member| 10 | target.send(member).each do |node| 11 | node.accept self 12 | end 13 | end 14 | @block.call target 15 | end 16 | 17 | visitor_for Selectors::Universal, Selectors::Simple do |target| 18 | target.children.each do |node| 19 | node.accept self 20 | end 21 | @block.call target 22 | end 23 | 24 | visitor_for CSS::Charset do |target| 25 | @block.call target 26 | end 27 | 28 | visitor_for CSS::ImportRule do |target| 29 | target.media.each do |node| 30 | node.accept self 31 | end 32 | @block.call target 33 | end 34 | 35 | visitor_for CSS::Media, 36 | Selectors::Id, 37 | Selectors::Class, 38 | Selectors::PseudoClass, 39 | Selectors::Attribute, 40 | Terms::Ident, 41 | Terms::String, 42 | Terms::URI, 43 | Terms::Number, 44 | Terms::Hash, 45 | Terms::Function, 46 | Terms::Rgb do |target| 47 | @block.call target 48 | end 49 | 50 | visitor_for CSS::Declaration do |target| 51 | target.expressions.each do |node| 52 | node.accept self 53 | end 54 | @block.call target 55 | end 56 | 57 | visitor_for CSS::RuleSet do |target| 58 | target.selectors.each do |node| 59 | node.accept self 60 | end 61 | target.declarations.each do |node| 62 | node.accept self 63 | end 64 | @block.call target 65 | end 66 | 67 | visitor_for Selector do |target| 68 | target.simple_selectors.each { |ss| ss.accept self } 69 | @block.call target 70 | end 71 | 72 | visitor_for Selectors::Type do |target| 73 | target.additional_selectors.each do |node| 74 | node.accept self 75 | end 76 | @block.call target 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/test_collection.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | class TestCollection < CSSPool::TestCase 5 | def test_new 6 | assert CSSPool::Collection.new 7 | assert CSSPool::Collection.new { |url| } 8 | end 9 | 10 | def test_append 11 | collection = CSSPool::Collection.new 12 | collection << "div { background: green; }" 13 | assert_equal 1, collection.length 14 | end 15 | 16 | def test_collection_imports_stuff 17 | called = false 18 | collection = CSSPool::Collection.new do |url| 19 | called = true 20 | assert_equal 'hello.css', url 21 | "div { background: red; }" 22 | end 23 | 24 | collection << '@import url(hello.css);' 25 | assert called, "block was not called" 26 | assert_equal 2, collection.length 27 | end 28 | 29 | def test_collection_imports_imports_imports 30 | css = { 31 | 'foo.css' => '@import url("bar.css");', 32 | 'bar.css' => '@import url("baz.css");', 33 | 'baz.css' => 'div { background: red; }', 34 | } 35 | 36 | collection = CSSPool::Collection.new do |url| 37 | css[url] || raise 38 | end 39 | 40 | collection << '@import url(foo.css);' 41 | assert_equal 4, collection.length 42 | assert_nil collection.last.parent 43 | assert_equal collection[-2].parent, collection.last 44 | assert_equal collection[-3].parent, collection[-2] 45 | assert_equal collection[-4].parent, collection[-3] 46 | end 47 | 48 | def test_load_only_once 49 | css = { 50 | 'foo.css' => '@import url("foo.css");', 51 | } 52 | 53 | collection = CSSPool::Collection.new do |url| 54 | css[url] || raise 55 | end 56 | 57 | collection << '@import url(foo.css);' 58 | 59 | assert_equal 2, collection.length 60 | end 61 | 62 | def test_each 63 | css = { 64 | 'foo.css' => '@import url("foo.css");', 65 | } 66 | 67 | collection = CSSPool::Collection.new do |url| 68 | css[url] || raise(url.inspect) 69 | end 70 | 71 | collection << '@import url(foo.css);' 72 | 73 | list = [] 74 | collection.each do |thing| 75 | list << thing 76 | end 77 | 78 | assert_equal 2, list.length 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/csspool/visitors/comparable.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Visitors 3 | class Comparable < Visitor 4 | def initialize other 5 | super() 6 | @other = other 7 | end 8 | 9 | visitor_for CSS::Document do |target| 10 | [ 11 | :parent, 12 | :charsets, 13 | :parent_import_rule, 14 | :import_rules, 15 | :rule_sets, 16 | ].all? { |m| target.send(m) == @other.send(m) } 17 | end 18 | 19 | visitor_for CSS::RuleSet do |target| 20 | [:selectors, :declarations, :media].all? { |m| 21 | target.send(m) == @other.send(m) 22 | } 23 | end 24 | 25 | visitor_for Selector do |target| 26 | target.simple_selectors == @other.simple_selectors 27 | end 28 | 29 | visitor_for CSS::ImportRule do |target| 30 | [:uri, :namespace, :media].all? { |m| 31 | target.send(m) == @other.send(m) 32 | } 33 | end 34 | 35 | visitor_for Selectors::PseudoClass do |target| 36 | [:name, :extra].all? { |m| 37 | target.send(m) == @other.send(m) 38 | } 39 | end 40 | 41 | visitor_for CSS::Media, Selectors::Id, Selectors::Class do |target| 42 | target.name == @other.name 43 | end 44 | 45 | visitor_for Selectors::Type, Selectors::Universal, 46 | Selectors::Simple do |target| 47 | [:name, :combinator, :additional_selectors].all? { |m| 48 | target.send(m) == @other.send(m) 49 | } 50 | end 51 | 52 | visitor_for CSS::Declaration do |target| 53 | [:property, :expressions, :important].all? { |m| 54 | target.send(m) == @other.send(m) 55 | } 56 | end 57 | 58 | visitor_for Terms::Function do |target| 59 | [:name, :params, :operator].all? { |m| 60 | target.send(m) == @other.send(m) 61 | } 62 | end 63 | 64 | visitor_for Terms::Number do |target| 65 | [:type, :unary_operator, :value, :operator].all? { |m| 66 | target.send(m) == @other.send(m) 67 | } 68 | end 69 | 70 | visitor_for Terms::URI,Terms::String,Terms::Ident,Terms::Hash do |target| 71 | [:value, :operator].all? { |m| target.send(m) == @other.send(m) } 72 | end 73 | 74 | visitor_for Terms::Rgb do |target| 75 | [ 76 | :red, 77 | :green, 78 | :blue, 79 | :operator 80 | ].all? { |m| target.send(m) == @other.send(m) } 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | == 3.0.2 2 | 3 | * New Features 4 | 5 | * Support IE Safe Hacks [stereobooster] 6 | 7 | * Bugfixes 8 | 9 | * Fix #23 - set up CSSPool.CSS so it can read files. [Mike Tierney] 10 | 11 | == 3.0.1 12 | 13 | * New Features 14 | 15 | * 'Minified' CSS output: passing :minify => true to a to_css call will omit 16 | line breaks 17 | * Added Gemfile 18 | 19 | * Bugfixes 20 | 21 | * Parse properties without semi-colons at the end of a declaration. 22 | [stereobooster] 23 | * Handle whitespaces after properties [stereobooster] 24 | 25 | == 3.0.0 26 | 27 | * New Features 28 | 29 | * Pure ruby: no longer uses C based back end. 30 | 31 | == 2.0.0 32 | 33 | * Bugfixes 34 | 35 | * Uh... Many. 36 | 37 | * New Features 38 | 39 | * Now wraps libcroco via FFI 40 | 41 | == 0.2.6 42 | 43 | * Fix comment greediness. [Seth Rasmussen] 44 | 45 | == 0.2.5 46 | 47 | * Accepting spaces after error rules. 48 | * Comments with asterisks tokenize correctly. [Seth Rasmussen] 49 | * Stop polluting the global namespace with includes. [Thomas Kadauke] 50 | 51 | == 0.2.4 52 | 53 | * Fixed error handling on at rules. Thanks Dan for tests! 54 | * Made specificity an array 55 | * Made StyleSheet::Rule comparable on specificity 56 | 57 | == 0.2.3 58 | 59 | * Fixed a bug where nil selectors on conditional selectors were being visited. 60 | 61 | == 0.2.2 62 | 63 | * I suck. 64 | 65 | == 0.2.1 66 | 67 | * Recovering from unexpected tokens in the properties section. 68 | 69 | == 0.2.0 70 | 71 | * Added CSS::SAC::Parser#parse_rule to parse a single rule. 72 | * Added CSS::StyleSheet#find_rule for finding a particular rule. 73 | * Added CSS::StyleSheet#rules_matching for finding all rules matching a node. 74 | * Added CSS::StyleSheet#create_rule for creating a new rule. 75 | * Added CSS::StyleSheet#find_all_rules_matching for finding all rules that match 76 | any node in the passed in document. 77 | * Added .eql? to selector AST 78 | * Added .hash to selector AST 79 | * Added .eql? to LexicalUnits 80 | * Added .hash to LexicalUnits 81 | * Added CSS::StyleSheet#to_css 82 | * Added CSS::StyleSheet#reduce! 83 | * CSS::StyleSheet is now the default document handler 84 | 85 | == 0.1.1 86 | 87 | * Added specificity to selectors. 88 | * Added == to selector ASTs. 89 | * Added =~ to selector ASTs. The object passed in to =~ must quack like an 90 | hpricot node. It must implement the following methods: name, parent, child, 91 | next_sibling, attributes. 92 | * Added .to_xpath to the selector AST. 93 | * Fixed a bug where functions as pseudo classes didn't parse. 94 | 95 | == 0.1.0 96 | 97 | * Birthday! 98 | 99 | -------------------------------------------------------------------------------- /test/visitors/test_comparable.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | module Visitors 5 | class TestComparable < CSSPool::TestCase 6 | def equalitest css 7 | doc1 = CSSPool.CSS css 8 | doc2 = CSSPool.CSS css 9 | assert_equal doc1, doc2 10 | 11 | list1 = [] 12 | list2 = [] 13 | 14 | doc1.each { |node| list1 << node } 15 | doc2.each { |node| list2 << node } 16 | 17 | assert_equal list1, list2 18 | 19 | stack = [doc1] 20 | until stack.empty? do 21 | stack += stack.pop.children 22 | end 23 | 24 | assert_equal doc1.hash, doc2.hash 25 | end 26 | 27 | def test_not_equal 28 | doc1 = CSSPool.CSS 'div { border: foo(1, 2); }' 29 | assert_not_equal nil, doc1 30 | end 31 | 32 | def test_hash_range 33 | equalitest 'div { border: #123; }' 34 | end 35 | 36 | def test_div_with_id 37 | equalitest 'div#foo { border: #123; }' 38 | end 39 | 40 | def test_div_with_pseudo 41 | equalitest 'div:foo { border: #123; }' 42 | end 43 | 44 | def test_div_with_universal 45 | equalitest '* { border: #123; }' 46 | end 47 | 48 | def test_simple 49 | equalitest '.foo { border: #123; }' 50 | end 51 | 52 | def test_rgb 53 | equalitest 'div { border: rgb(1,2,3); }' 54 | end 55 | 56 | def test_rgb_with_percentage 57 | equalitest 'div { border: rgb(100%,2%,3%); }' 58 | end 59 | 60 | def test_negative_number 61 | equalitest 'div { border: -1px; }' 62 | end 63 | 64 | def test_positive_number 65 | equalitest 'div { border: 1px; }' 66 | end 67 | 68 | %w{ 69 | 1 1em 1ex 1px 1in 1cm 1mm 1pt 1pc 1% 1deg 1rad 1ms 1s 1Hz 1kHz 70 | }.each do |num| 71 | define_method(:"test_num_#{num}") do 72 | equalitest "div { border: #{num}; }" 73 | end 74 | end 75 | 76 | def test_string_term 77 | equalitest 'div { border: "hello"; }' 78 | end 79 | 80 | def test_inherit 81 | equalitest 'div { color: inherit; }' 82 | end 83 | 84 | def test_important 85 | equalitest 'div { color: inherit !important; }' 86 | end 87 | 88 | def test_function 89 | equalitest 'div { border: foo("hello"); }' 90 | end 91 | 92 | def test_uri 93 | equalitest 'div { border: url(http://tenderlovemaking.com/); }' 94 | end 95 | 96 | def test_import 97 | equalitest '@import "foo.css" screen, print;' 98 | equalitest '@import "foo.css";' 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/test_parser.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | class TestParser < CSSPool::TestCase 5 | def test_empty_doc_on_blank 6 | assert CSSPool.CSS(nil) 7 | assert CSSPool.CSS('') 8 | end 9 | 10 | def test_doc_charset 11 | doc = CSSPool.CSS <<-eocss 12 | @charset "UTF-8"; 13 | @import url("foo.css") screen; 14 | div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 15 | eocss 16 | assert_equal 'UTF-8', doc.charsets.first.name 17 | end 18 | 19 | def test_doc_parser 20 | doc = CSSPool.CSS <<-eocss 21 | @charset "UTF-8"; 22 | div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 23 | eocss 24 | 25 | assert_equal 1, doc.rule_sets.length 26 | rule_set = doc.rule_sets.first 27 | assert_equal 4, rule_set.selectors.length 28 | assert_equal 1, rule_set.declarations.length 29 | assert_equal 'background', rule_set.declarations.first.property 30 | end 31 | 32 | def test_media 33 | doc = CSSPool.CSS <<-eocss 34 | @media print { 35 | div { background: red, blue; } 36 | } 37 | eocss 38 | assert_equal 1, doc.rule_sets.first.media.length 39 | end 40 | 41 | def test_universal_to_css 42 | doc = CSSPool.CSS <<-eocss 43 | * { background: red, blue; } 44 | eocss 45 | assert_match '*', doc.to_css 46 | end 47 | 48 | def test_doc_to_css 49 | doc = CSSPool.CSS <<-eocss 50 | div#a, a.foo, a:hover, a[href][int="10"]{ background: red, blue; } 51 | eocss 52 | assert_match 'div#a, a.foo, a:hover, a[href][int="10"]', doc.to_css 53 | assert_match 'background: red, blue;', doc.to_css 54 | end 55 | 56 | def test_doc_desc_to_css 57 | doc = CSSPool.CSS <<-eocss 58 | div > a { background: #123; } 59 | eocss 60 | assert_match 'div > a', doc.to_css 61 | end 62 | 63 | def test_doc_pseudo_to_css 64 | doc = CSSPool.CSS <<-eocss 65 | :hover { background: #123; } 66 | eocss 67 | assert_match ':hover', doc.to_css 68 | end 69 | 70 | def test_doc_id_to_css 71 | doc = CSSPool.CSS <<-eocss 72 | #hover { background: #123; } 73 | eocss 74 | assert_match '#hover', doc.to_css 75 | end 76 | 77 | def test_important 78 | doc = CSSPool.CSS <<-eocss 79 | div > a { background: #123 !important; } 80 | eocss 81 | assert_match '!important', doc.to_css 82 | end 83 | 84 | def test_doc_func_to_css 85 | doc = CSSPool.CSS <<-eocss 86 | div { border: foo(1, 2); } 87 | eocss 88 | assert_match('foo(1, 2)', doc.to_css) 89 | end 90 | 91 | def test_missing_semicolon 92 | doc = CSSPool.CSS <<-eocss 93 | div { border: none } 94 | eocss 95 | assert_match('none', doc.to_css) 96 | doc = CSSPool.CSS <<-eocss 97 | div { border: none; background: #fff } 98 | eocss 99 | assert_match('none', doc.to_css) 100 | assert_match('#fff', doc.to_css) 101 | end 102 | 103 | def test_whitespaces 104 | doc = CSSPool.CSS <<-eocss 105 | div { border : none; } 106 | eocss 107 | assert_match('none', doc.to_css) 108 | assert_match('border:', doc.to_css) 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/csspool/css/tokenizer.rex: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module CSS 3 | class Tokenizer < Parser 4 | 5 | macro 6 | nl \n|\r\n|\r|\f 7 | w [\s]* 8 | nonascii [^\0-\177] 9 | num ([0-9]*\.[0-9]+|[0-9]+) 10 | unicode \\[0-9A-Fa-f]{1,6}(\r\n|[\s])? 11 | 12 | escape {unicode}|\\[^\n\r\f0-9A-Fa-f] 13 | nmchar [_A-Za-z0-9-]|{nonascii}|{escape} 14 | nmstart [_A-Za-z]|{nonascii}|{escape} 15 | ident [-@]?({nmstart})({nmchar})* 16 | func [-@]?({nmstart})({nmchar}|[.])* 17 | name ({nmchar})+ 18 | string1 "([^\n\r\f\\"]|\\{nl}|{nonascii}|{escape})*" 19 | string2 '([^\n\r\f\\']|\\{nl}|{nonascii}|{escape})*' 20 | string ({string1}|{string2}) 21 | invalid1 "([^\n\r\f\\"]|\\{nl}|{nonascii}|{escape})* 22 | invalid2 '([^\n\r\f\\']|\\{nl}|{nonascii}|{escape})* 23 | invalid ({invalid1}|{invalid2}) 24 | comment \/\*(.|{w})*?\*\/ 25 | 26 | rule 27 | 28 | # [:state] pattern [actions] 29 | 30 | url\({w}{string}{w}\) { [:URI, st(text)] } 31 | url\({w}([!#\$%&*-~]|{nonascii}|{escape})*{w}\) { [:URI, st(text)] } 32 | U\+[0-9a-fA-F?]{1,6}(-[0-9a-fA-F]{1,6})? {[:UNICODE_RANGE, st(text)] } 33 | {w}{comment}{w} { next_token } 34 | 35 | {func}\(\s* { [:FUNCTION, st(text)] } 36 | {w}@import{w} { [:IMPORT_SYM, st(text)] } 37 | {w}@page{w} { [:PAGE_SYM, st(text)] } 38 | {w}@charset{w} { [:CHARSET_SYM, st(text)] } 39 | {w}@media{w} { [:MEDIA_SYM, st(text)] } 40 | {w}!({w}|{w}{comment}{w})important{w} { [:IMPORTANT_SYM, st(text)] } 41 | {ident} { [:IDENT, st(text)] } 42 | \#{name} { [:HASH, st(text)] } 43 | {w}~={w} { [:INCLUDES, st(text)] } 44 | {w}\|={w} { [:DASHMATCH, st(text)] } 45 | {w}\^={w} { [:PREFIXMATCH, st(text)] } 46 | {w}\$={w} { [:SUFFIXMATCH, st(text)] } 47 | {w}\*={w} { [:SUBSTRINGMATCH, st(text)] } 48 | {w}!={w} { [:NOT_EQUAL, st(text)] } 49 | {w}={w} { [:EQUAL, st(text)] } 50 | {w}\) { [:RPAREN, st(text)] } 51 | {w}\[{w} { [:LSQUARE, st(text)] } 52 | {w}\] { [:RSQUARE, st(text)] } 53 | {w}\+{w} { [:PLUS, st(text)] } 54 | {w}\{{w} { [:LBRACE, st(text)] } 55 | {w}\}{w} { [:RBRACE, st(text)] } 56 | {w}>{w} { [:GREATER, st(text)] } 57 | {w},{w} { [:COMMA, st(',')] } 58 | {w};{w} { [:SEMI, st(';')] } 59 | \* { [:STAR, st(text)] } 60 | {w}~{w} { [:TILDE, st(text)] } 61 | \:not\({w} { [:NOT, st(text)] } 62 | {w}{num}em{w} { [:EMS, st(text)] } 63 | {w}{num}ex{w} { [:EXS, st(text)] } 64 | 65 | {w}{num}(px|cm|mm|in|pt|pc){w} { [:LENGTH, st(text)] } 66 | {w}{num}(deg|rad|grad){w} { [:ANGLE, st(text)] } 67 | {w}{num}(ms|s){w} { [:TIME, st(text)] } 68 | {w}{num}[k]?hz{w} { [:FREQ, st(text)] } 69 | 70 | {w}{num}%{w} { [:PERCENTAGE, st(text)] } 71 | {w}{num}{w} { [:NUMBER, st(text)] } 72 | {w}\/\/{w} { [:DOUBLESLASH, st(text)] } 73 | {w}\/{w} { [:SLASH, st('/')] } 74 | { [:CDC, st(text)] } 76 | {w}\-(?!{ident}){w} { [:MINUS, st(text)] } 77 | {w}\+{w} { [:PLUS, st(text)] } 78 | 79 | 80 | [\s]+ { [:S, st(text)] } 81 | {string} { [:STRING, st(text)] } 82 | {invalid} { [:INVALID, st(text)] } 83 | . { [st(text), st(text)] } 84 | 85 | inner 86 | 87 | def st o 88 | @st ||= Hash.new { |h,k| h[k] = k } 89 | @st[o] 90 | end 91 | 92 | end 93 | end 94 | end 95 | 96 | # vim: syntax=lex 97 | -------------------------------------------------------------------------------- /test/sac/test_parser.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | module CSSPool 4 | module SAC 5 | class TestParser < CSSPool::TestCase 6 | def setup 7 | super 8 | @doc = MyDoc.new 9 | @css = <<-eocss 10 | @charset "UTF-8"; 11 | @import url("foo.css") screen; 12 | /* This is a comment */ 13 | div a.foo, #bar, * { background: red; } 14 | div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 15 | eocss 16 | @parser = CSSPool::SAC::Parser.new(@doc) 17 | @parser.parse(@css) 18 | end 19 | 20 | def test_start_and_end_called_with_the_same 21 | assert_equal @doc.start_selectors, @doc.end_selectors 22 | end 23 | 24 | def test_parse_no_doc 25 | parser = CSSPool::SAC::Parser.new 26 | parser.parse(@css) 27 | end 28 | 29 | def test_start_document 30 | assert_equal [true], @doc.start_documents 31 | end 32 | 33 | def test_end_document 34 | assert_equal [true], @doc.end_documents 35 | end 36 | 37 | def test_charset 38 | assert_equal("UTF-8", @doc.charsets.first.first) 39 | end 40 | 41 | def test_import_style 42 | styles = @doc.import_styles.first 43 | assert_equal "screen", styles.first.first.value 44 | assert_equal "foo.css", styles[1].value 45 | assert_nil styles[2] 46 | end 47 | 48 | def test_start_selector 49 | selectors_for_rule = @doc.start_selectors.first 50 | assert selectors_for_rule 51 | assert_equal 3, selectors_for_rule.length 52 | end 53 | 54 | def test_simple_selector 55 | selectors_for_rule = @doc.start_selectors.first 56 | selector = selectors_for_rule.first # => div a.foo 57 | assert_equal 2, selector.simple_selectors.length 58 | end 59 | 60 | def test_additional_selector_list 61 | selectors_for_rule = @doc.start_selectors.first 62 | selector = selectors_for_rule.first # => div a.foo 63 | simple_selector = selector.simple_selectors[1] # => a.foo 64 | assert additional_selectors = simple_selector.additional_selectors 65 | assert_equal 1, additional_selectors.length 66 | end 67 | 68 | def test_additional_selector_class_name 69 | selectors_for_rule = @doc.start_selectors.first 70 | selector = selectors_for_rule.first # => div a.foo 71 | simple_selector = selector.simple_selectors[1] # => a.foo 72 | assert additional_selectors = simple_selector.additional_selectors 73 | foo_class = additional_selectors.first 74 | assert_equal 'foo', foo_class.name 75 | end 76 | 77 | # div#a, a.foo, a:hover, a[href='watever'] { background: red; } 78 | def test_id_additional_selector 79 | selectors_for_rule = @doc.start_selectors[1] 80 | selector = selectors_for_rule.first # => div#a 81 | simple_selector = selector.simple_selectors.first # => div#a 82 | assert_equal 'a', simple_selector.additional_selectors.first.name 83 | end 84 | 85 | # div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 86 | def test_pseudo_additional_selector 87 | selectors_for_rule = @doc.start_selectors[1] 88 | selector = selectors_for_rule[2] # => 'a:hover' 89 | simple_selector = selector.simple_selectors.first # => a:hover 90 | assert_equal 'hover', simple_selector.additional_selectors.first.name 91 | assert_nil simple_selector.additional_selectors.first.extra 92 | end 93 | 94 | # div#a, a.foo, a:hover, a[href][int="10"]{ background: red; } 95 | def test_attribute_selector 96 | selectors_for_rule = @doc.start_selectors[1] 97 | selector = selectors_for_rule[3] # => a[href][int="10"] 98 | simple_selector = selector.simple_selectors.first 99 | 100 | assert_equal 'href', simple_selector.additional_selectors.first.name 101 | assert_nil simple_selector.additional_selectors.first.value 102 | assert_equal 1, simple_selector.additional_selectors.first.match_way 103 | 104 | assert_equal 'int', simple_selector.additional_selectors[1].name 105 | assert_equal '10', simple_selector.additional_selectors[1].value 106 | assert_equal 2, simple_selector.additional_selectors[1].match_way 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/csspool/visitors/to_css.rb: -------------------------------------------------------------------------------- 1 | module CSSPool 2 | module Visitors 3 | class ToCSS < Visitor 4 | 5 | CSS_IDENTIFIER_ILLEGAL_CHARACTERS = 6 | (0..255).to_a.pack('U*').gsub(/[a-zA-Z0-9_-]/, '') 7 | CSS_STRING_ESCAPE_MAP = { 8 | "\\" => "\\\\", 9 | "\"" => "\\\"", 10 | "\n" => "\\a ", # CSS2 4.1.3 p3.2 11 | "\r" => "\\\r", 12 | "\f" => "\\\f" 13 | } 14 | 15 | def initialize 16 | @indent_level = 0 17 | @indent_space = indent_space 18 | end 19 | 20 | visitor_for CSS::Document do |target| 21 | # Default media list is [] 22 | current_media_type = [] 23 | 24 | tokens = [] 25 | 26 | target.charsets.each do |char_set| 27 | tokens << char_set.accept(self) 28 | end 29 | 30 | target.import_rules.each do |ir| 31 | tokens << ir.accept(self) 32 | end 33 | 34 | target.rule_sets.each { |rs| 35 | if rs.media != current_media_type 36 | media = " " + rs.media.map do |medium| 37 | escape_css_identifier medium.name.value 38 | end.join(', ') 39 | tokens << "#{indent}@media#{media} {" 40 | @indent_level += 1 41 | end 42 | 43 | tokens << rs.accept(self) 44 | 45 | if rs.media != current_media_type 46 | current_media_type = rs.media 47 | @indent_level -= 1 48 | tokens << "#{indent}}" 49 | end 50 | } 51 | tokens.join(line_break) 52 | end 53 | 54 | visitor_for CSS::Charset do |target| 55 | "@charset \"#{escape_css_string target.name}\";" 56 | end 57 | 58 | visitor_for CSS::ImportRule do |target| 59 | media = '' 60 | media = " " + target.media.map do |medium| 61 | escape_css_identifier medium.name.value 62 | end.join(', ') if target.media.length > 0 63 | 64 | "#{indent}@import #{target.uri.accept(self)}#{media};" 65 | end 66 | 67 | visitor_for CSS::RuleSet do |target| 68 | "#{indent}" + 69 | target.selectors.map { |sel| sel.accept self }.join(", ") + " {#{line_break}" + 70 | target.declarations.map { |decl| decl.accept self }.join(line_break) + 71 | "#{line_break}#{indent}}" 72 | end 73 | 74 | visitor_for CSS::Declaration do |target| 75 | important = target.important? ? ' !important' : '' 76 | 77 | indent { 78 | "#{indent}#{escape_css_identifier target.property}: " + target.expressions.map { |exp| 79 | 80 | op = '/' == exp.operator ? ' /' : exp.operator 81 | 82 | [ 83 | op, 84 | exp.accept(self), 85 | ].join ' ' 86 | }.join.strip + "#{important};" 87 | } 88 | end 89 | 90 | visitor_for Terms::Ident do |target| 91 | escape_css_identifier target.value 92 | end 93 | 94 | visitor_for Terms::Hash do |target| 95 | "##{target.value}" 96 | end 97 | 98 | visitor_for Selectors::Simple, Selectors::Universal do |target| 99 | ([target.name] + target.additional_selectors.map { |x| 100 | x.accept self 101 | }).join 102 | end 103 | 104 | visitor_for Terms::URI do |target| 105 | "url(\"#{escape_css_string target.value}\")" 106 | end 107 | 108 | visitor_for Terms::Function do |target| 109 | "#{escape_css_identifier target.name}(" + 110 | target.params.map { |x| 111 | [ 112 | x.operator, 113 | x.accept(self) 114 | ].compact.join(' ') 115 | }.join + ')' 116 | end 117 | 118 | visitor_for Terms::Rgb do |target| 119 | params = [ 120 | target.red, 121 | target.green, 122 | target.blue 123 | ].map { |c| 124 | c.accept(self) 125 | }.join ', ' 126 | 127 | %{rgb(#{params})} 128 | end 129 | 130 | visitor_for Terms::String do |target| 131 | "\"#{escape_css_string target.value}\"" 132 | end 133 | 134 | visitor_for Selector do |target| 135 | target.simple_selectors.map { |ss| ss.accept self }.join 136 | end 137 | 138 | visitor_for Selectors::Type do |target| 139 | combo = { 140 | :s => ' ', 141 | :+ => ' + ', 142 | :> => ' > ' 143 | }[target.combinator] 144 | 145 | name = target.name == '*' ? '*' : escape_css_identifier(target.name) 146 | [combo, name].compact.join + 147 | target.additional_selectors.map { |as| as.accept self }.join 148 | end 149 | 150 | visitor_for Terms::Number do |target| 151 | [ 152 | target.unary_operator == :minus ? '-' : nil, 153 | target.value, 154 | target.type 155 | ].compact.join 156 | end 157 | 158 | visitor_for Selectors::Id do |target| 159 | "##{escape_css_identifier target.name}" 160 | end 161 | 162 | visitor_for Selectors::Class do |target| 163 | ".#{escape_css_identifier target.name}" 164 | end 165 | 166 | visitor_for Selectors::PseudoClass do |target| 167 | if target.extra.nil? 168 | ":#{escape_css_identifier target.name}" 169 | else 170 | ":#{escape_css_identifier target.name}(#{escape_css_identifier target.extra})" 171 | end 172 | end 173 | 174 | visitor_for Selectors::Attribute do |target| 175 | case target.match_way 176 | when Selectors::Attribute::SET 177 | "[#{escape_css_identifier target.name}]" 178 | when Selectors::Attribute::EQUALS 179 | "[#{escape_css_identifier target.name}=\"#{escape_css_string target.value}\"]" 180 | when Selectors::Attribute::INCLUDES 181 | "[#{escape_css_identifier target.name} ~= \"#{escape_css_string target.value}\"]" 182 | when Selectors::Attribute::DASHMATCH 183 | "[#{escape_css_identifier target.name} |= \"#{escape_css_string target.value}\"]" 184 | else 185 | raise "no matching matchway" 186 | end 187 | end 188 | 189 | private 190 | def indent 191 | if block_given? 192 | @indent_level += 1 193 | result = yield 194 | @indent_level -= 1 195 | return result 196 | end 197 | "#{@indent_space * @indent_level}" 198 | end 199 | 200 | def line_break 201 | "\n" 202 | end 203 | 204 | def indent_space 205 | ' ' 206 | end 207 | 208 | def escape_css_identifier text 209 | # CSS2 4.1.3 p2 210 | unsafe_chars = /[#{Regexp.escape CSS_IDENTIFIER_ILLEGAL_CHARACTERS}]/ 211 | text.gsub(/^\d|^\-(?=\-|\d)|#{unsafe_chars}/um) do |char| 212 | if ':()-\\ ='.include? char 213 | "\\#{char}" 214 | else # I don't trust others to handle space termination well. 215 | "\\#{char.unpack('U').first.to_s(16).rjust(6, '0')}" 216 | end 217 | end 218 | end 219 | 220 | def escape_css_string text 221 | text.gsub(/[\\"\n\r\f]/) {CSS_STRING_ESCAPE_MAP[$&]} 222 | end 223 | end 224 | 225 | class ToMinifiedCSS < ToCSS 226 | def line_break 227 | "" 228 | end 229 | 230 | def indent_space 231 | ' ' 232 | end 233 | 234 | visitor_for CSS::RuleSet do |target| 235 | target.selectors.map { |sel| sel.accept self }.join(", ") + " {" + 236 | target.declarations.map { |decl| decl.accept self }.join + 237 | " }" 238 | end 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /test/sac/test_terms.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'helper' 3 | 4 | module CSSPool 5 | module SAC 6 | class TestTerms < CSSPool::TestCase 7 | def setup 8 | @doc = MyDoc.new 9 | @parser = CSSPool::SAC::Parser.new(@doc) 10 | end 11 | 12 | def test_hash_range 13 | @parser.parse <<-eocss 14 | div { border: #123; } 15 | eocss 16 | hash = @doc.properties.first[1].first 17 | assert_equal '123', hash.value 18 | end 19 | 20 | def test_rgb 21 | @parser.parse <<-eocss 22 | div { border: rgb(1,2,3); } 23 | eocss 24 | color = @doc.properties.first[1].first 25 | assert_equal 1, color.red.value 26 | assert_equal 2, color.green.value 27 | assert_equal 3, color.blue.value 28 | assert_match('rgb(1, 2, 3)', color.to_css) 29 | end 30 | 31 | def test_rgb_with_percentage 32 | @parser.parse <<-eocss 33 | div { border: rgb(100%, 2%, 3%); } 34 | eocss 35 | color = @doc.properties.first[1].first 36 | assert_equal 100, color.red.value 37 | assert_equal 2, color.green.value 38 | assert_equal 3, color.blue.value 39 | assert_match('rgb(100%, 2%, 3%)', color.to_css) 40 | end 41 | 42 | def test_negative_number 43 | @parser.parse <<-eocss 44 | div { border: -1px; } 45 | eocss 46 | assert_equal 1, @doc.properties.length 47 | size = @doc.properties.first[1].first 48 | assert_equal :minus, size.unary_operator 49 | assert_equal 1, size.value 50 | assert_equal '-1px', size.to_s 51 | assert_equal '-1px', size.to_css 52 | end 53 | 54 | def test_positive_number 55 | @parser.parse <<-eocss 56 | div { border: 1px; } 57 | eocss 58 | assert_equal 1, @doc.properties.length 59 | size = @doc.properties.first[1].first 60 | assert_equal 1, size.value 61 | assert_equal '1px', size.to_s 62 | end 63 | 64 | %w{ 65 | 1 1em 1ex 1px 1in 1cm 1mm 1pt 1pc 1% 1deg 1rad 1ms 1s 1Hz 1kHz 66 | }.each do |num| 67 | define_method(:"test_num_#{num}") do 68 | @parser.parse <<-eocss 69 | div { border: #{num}; } 70 | eocss 71 | assert_equal 1, @doc.properties.length 72 | size = @doc.properties.first[1].first 73 | assert_equal 1, size.value 74 | assert_equal num, size.to_s 75 | assert_equal num, size.to_css 76 | end 77 | end 78 | 79 | def test_selector_attribute 80 | @parser.parse <<-eocss 81 | div[attr = value] { } 82 | div[attr\\== value] { } 83 | div[attr="\\"quotes\\""] { } 84 | div[attr = unicode\\ \\1D11E\\BF ] { } 85 | eocss 86 | 87 | attrs = @doc.end_selectors.flatten.map(&:simple_selectors).flatten.map(&:additional_selectors).flatten 88 | assert_equal 4, attrs.length 89 | 90 | attrs.shift.tap do |attr| 91 | assert_equal "attr", attr.name, 92 | "Interprets name." 93 | assert_equal "value", attr.value, 94 | "Interprets bare value." 95 | end 96 | 97 | assert_equal "attr=", attrs.shift.name, 98 | "Interprets identifier escapes." 99 | 100 | assert_equal "\"quotes\"", attrs.shift.value, 101 | "Interprets quoted values." 102 | 103 | assert_equal "unicode \360\235\204\236\302\277", attrs.shift.value, 104 | "Interprets unicode escapes." 105 | end 106 | 107 | def test_string_term 108 | @parser.parse <<-eocss 109 | div { content: "basic"; } 110 | div { content: "\\"quotes\\""; } 111 | div { content: "unicode \\1D11E\\BF "; } 112 | div { content: "contin\\\nuation"; } 113 | div { content: "new\\aline"; } 114 | div { content: "\\11FFFF "; } 115 | eocss 116 | terms = @doc.properties.map {|s| s[1].first} 117 | assert_equal 6, terms.length 118 | 119 | assert_equal 'basic', terms.shift.value, 120 | "Recognizes a basic string" 121 | 122 | assert_equal "\"quotes\"", terms.shift.value, 123 | "Recognizes strings containing quotes." 124 | 125 | assert_equal "unicode \360\235\204\236\302\277", terms.shift.value, 126 | "Interprets unicode escapes." 127 | 128 | assert_equal "continuation", terms.shift.value, 129 | "Supports line continuation." 130 | 131 | assert_equal "new\nline", terms.shift.value, 132 | "Interprets newline escape." 133 | 134 | assert_equal "\357\277\275", terms.shift.value, 135 | "Kills absurd characters." 136 | end 137 | 138 | def test_inherit 139 | @parser.parse <<-eocss 140 | div { color: inherit; } 141 | eocss 142 | assert_equal 1, @doc.properties.length 143 | string = @doc.properties.first[1].first 144 | assert_equal 'inherit', string.value 145 | assert_equal 'inherit', string.to_css 146 | end 147 | 148 | def test_important 149 | @parser.parse <<-eocss 150 | div { color: inherit !important; } 151 | eocss 152 | assert_equal 1, @doc.properties.length 153 | string = @doc.properties.first[1].first 154 | assert_equal 'inherit', string.value 155 | assert_equal 'inherit', string.to_css 156 | end 157 | 158 | def test_declaration 159 | @parser.parse <<-eocss 160 | div { property: value; } 161 | div { colon\\:: value; } 162 | div { space\\ : value; } 163 | eocss 164 | properties = @doc.properties.map {|s| s[0]} 165 | assert_equal 3, properties.length 166 | 167 | assert_equal 'property', properties.shift, 168 | "Recognizes basic function." 169 | 170 | assert_equal 'colon:', properties.shift, 171 | "Recognizes property with escaped COLON." 172 | 173 | assert_equal 'space ', properties.shift, 174 | "Recognizes property with escaped SPACE." 175 | end 176 | 177 | def test_function 178 | @parser.parse <<-eocss 179 | div { content: attr(\"value\", ident); } 180 | div { content: \\30(\"value\", ident); } 181 | div { content: a\\ function(\"value\", ident); } 182 | div { content: a\\((\"value\", ident); } 183 | eocss 184 | terms = @doc.properties.map {|s| s[1].first} 185 | assert_equal 4, terms.length 186 | 187 | assert_equal 'attr', terms.shift.name, 188 | "Recognizes basic function." 189 | 190 | assert_equal '0', terms.shift.name, 191 | "Recognizes numeric function." 192 | 193 | assert_equal 'a function', terms.shift.name, 194 | "Recognizes function with escaped SPACE." 195 | 196 | assert_equal 'a(', terms.shift.name, 197 | "Recognizes function with escaped LPAREN." 198 | end 199 | 200 | def test_uri 201 | @parser.parse <<-eocss 202 | div { background: url(http://example.com/); } 203 | div { background: url( http://example.com/ ); } 204 | div { background: url("http://example.com/"); } 205 | div { background: url( " http://example.com/ " ); } 206 | div { background: url(http://example.com/\\"); } 207 | eocss 208 | terms = @doc.properties.map {|s| s[1].first} 209 | assert_equal 5, terms.length 210 | 211 | assert_equal 'http://example.com/', terms.shift.value, 212 | "Recognizes bare URI." 213 | 214 | assert_equal 'http://example.com/', terms.shift.value, 215 | "Recognize URI with spaces" 216 | 217 | assert_equal 'http://example.com/', terms.shift.value, 218 | "Recognize quoted URI" 219 | 220 | assert_equal ' http://example.com/ ', terms.shift.value, 221 | "Recognize quoted URI" 222 | 223 | assert_equal 'http://example.com/"', terms.shift.value, 224 | "Recognizes bare URI with quotes" 225 | end 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /test/css/test_tokenizer.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | require "helper" 4 | 5 | module CSSPool 6 | module CSS 7 | class TestTokenizer < CSSPool::TestCase 8 | def setup 9 | super 10 | @scanner = Class.new(CSSPool::CSS::Tokenizer) { 11 | def do_parse 12 | end 13 | }.new 14 | end 15 | 16 | { 17 | 'em' => :EMS, 18 | 'ex' => :EXS, 19 | 'px' => :LENGTH, 20 | 'cm' => :LENGTH, 21 | 'mm' => :LENGTH, 22 | 'in' => :LENGTH, 23 | 'pt' => :LENGTH, 24 | 'pc' => :LENGTH, 25 | 'deg' => :ANGLE, 26 | 'rad' => :ANGLE, 27 | 'grad' => :ANGLE, 28 | 'ms' => :TIME, 29 | 's' => :TIME, 30 | 'hz' => :FREQ, 31 | 'khz' => :FREQ, 32 | '%' => :PERCENTAGE, 33 | }.each do |unit,sym| 34 | define_method :"test_#{unit}" do 35 | ['10', '0.1'].each do |num| 36 | num = "#{num}#{unit}" 37 | [num, " #{num}", "#{num} ", " #{num} "].each do |str| 38 | @scanner.scan str 39 | assert_tokens([[sym, str]], @scanner) 40 | end 41 | end 42 | end 43 | end 44 | 45 | def test_num 46 | ['10', '0.1'].each do |num| 47 | [num, " #{num}", "#{num} ", " #{num} "].each do |str| 48 | @scanner.scan str 49 | assert_tokens([[:NUMBER, str]], @scanner) 50 | end 51 | end 52 | end 53 | 54 | def test_important 55 | [ 56 | '!important', 57 | ' !important', 58 | '!important ', 59 | '! important ', 60 | ' ! important ', 61 | ].each do |str| 62 | @scanner.scan str 63 | assert_tokens([[:IMPORTANT_SYM, str]], @scanner) 64 | end 65 | end 66 | 67 | { 68 | '@page' => :PAGE_SYM, 69 | '@import' => :IMPORT_SYM, 70 | '@media' => :MEDIA_SYM, 71 | '@charset' => :CHARSET_SYM, 72 | }.each do |k,v| 73 | define_method(:"test_#{k.sub(/@/, '')}") do 74 | [k, " #{k}", "#{k} ", " #{k} "].each do |str| 75 | @scanner.scan str 76 | assert_tokens([[v, str]], @scanner) 77 | end 78 | end 79 | end 80 | 81 | def test_invalid 82 | str = "'internet" 83 | @scanner.scan str 84 | assert_tokens([[:INVALID, str]], @scanner) 85 | 86 | str = '"internet' 87 | @scanner.scan str 88 | assert_tokens([[:INVALID, str]], @scanner) 89 | end 90 | 91 | #def test_comment 92 | # str = "/**** Hello World ***/" 93 | # @scanner.scan str 94 | # assert_tokens([[:COMMENT, str]], @scanner) 95 | 96 | # str = "/* Hello World */" 97 | # @scanner.scan str 98 | # assert_tokens([[:COMMENT, str]], @scanner) 99 | #end 100 | 101 | def test_rbrace 102 | str = " } \n " 103 | @scanner.scan str 104 | assert_tokens([[:RBRACE, str]], @scanner) 105 | end 106 | 107 | def test_lbrace 108 | str = " { " 109 | @scanner.scan str 110 | assert_tokens([[:LBRACE, str]], @scanner) 111 | end 112 | 113 | def test_semi 114 | str = " ; " 115 | @scanner.scan str 116 | assert_tokens([[:SEMI, ';']], @scanner) 117 | end 118 | 119 | def test_cdc 120 | @scanner.scan("-->") 121 | assert_tokens([[:CDC, "-->"]], @scanner) 122 | end 123 | 124 | def test_cdo 125 | @scanner.scan("