css_parser_master 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/CHANGELOG +34 -0
- data/LICENSE +21 -0
- data/README +0 -0
- data/README.rdoc +77 -0
- data/Rakefile.rb +18 -0
- data/VERSION +1 -0
- data/lib/css_parser_master.rb +156 -0
- data/lib/css_parser_master/declaration.rb +31 -0
- data/lib/css_parser_master/declaration_api.rb +91 -0
- data/lib/css_parser_master/declarations.rb +23 -0
- data/lib/css_parser_master/parser.rb +388 -0
- data/lib/css_parser_master/regexps.rb +46 -0
- data/lib/css_parser_master/rule_set.rb +337 -0
- data/lib/css_parser_master/selector.rb +33 -0
- data/lib/css_parser_master/selectors.rb +27 -0
- data/test/fixtures/import-circular-reference.css +4 -0
- data/test/fixtures/import-with-media-types.css +3 -0
- data/test/fixtures/import1.css +3 -0
- data/test/fixtures/simple.css +6 -0
- data/test/fixtures/subdir/import2.css +3 -0
- data/test/test_css_parser_basic.rb +84 -0
- data/test/test_css_parser_loading.rb +110 -0
- data/test/test_css_parser_media_types.rb +71 -0
- data/test/test_css_parser_misc.rb +151 -0
- data/test/test_css_parser_regexps.rb +69 -0
- data/test/test_helper.rb +6 -0
- data/test/test_merging.rb +88 -0
- data/test/test_rule_set.rb +90 -0
- data/test/test_rule_set_creating_shorthand.rb +90 -0
- data/test/test_rule_set_expanding_shorthand.rb +179 -0
- data/test/test_ruleset_expand.rb +40 -0
- data/test/test_selector.rb +26 -0
- data/test/test_selector_parsing.rb +27 -0
- metadata +112 -0
data/.gitignore
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
=== Ruby CSS Parser CHANGELOG
|
2
|
+
|
3
|
+
==== Version 1.1.2
|
4
|
+
* Extracted declarations API from RuleSet
|
5
|
+
* Added declarations API to both RuleSet and Selector classes
|
6
|
+
* Tests still work!
|
7
|
+
|
8
|
+
TODO
|
9
|
+
Use enumerables Declarations and Selectors instead of simple string arrays for much greater power and flexibility!
|
10
|
+
|
11
|
+
==== Version 1.1.1
|
12
|
+
* Encapsulated selector into its own class
|
13
|
+
* Encapsulated declaration into its own class
|
14
|
+
* Gem now uses Jewler (new Rakefile.rb created)
|
15
|
+
* All tests work with new selector and declaration iteration strategies which now returns a Selector and Declaration instance
|
16
|
+
|
17
|
+
==== Version 1.1.0
|
18
|
+
* Added support for local @import
|
19
|
+
* Better remote @import handling
|
20
|
+
|
21
|
+
==== Version 1.0.1
|
22
|
+
* Fallback for declartions without sort order
|
23
|
+
|
24
|
+
==== Version 1.0.0
|
25
|
+
* Various test fixes and udpate for Ruby 1.9 (thanks to Tyler Cunnion)
|
26
|
+
* Allow setting CSS declarations to nil
|
27
|
+
|
28
|
+
==== Version 0.9
|
29
|
+
* Initial version forked from Premailer project
|
30
|
+
|
31
|
+
==== TODO: Future
|
32
|
+
* border shorthand/folding support
|
33
|
+
* re-implement caching on CssParser.merge
|
34
|
+
* correctly parse http://www.webstandards.org/files/acid2/test.html
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
=== Ruby CSS Parser Master License
|
2
|
+
|
3
|
+
Copyright (c) 2010-05 Kristian Mandrup
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README
ADDED
File without changes
|
data/README.rdoc
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
= Ruby CSS Parser
|
2
|
+
|
3
|
+
Load, parse and cascade CSS rule sets in Ruby.
|
4
|
+
|
5
|
+
=== Setup
|
6
|
+
|
7
|
+
Install the gem.
|
8
|
+
|
9
|
+
gem install css_parser
|
10
|
+
|
11
|
+
Done.
|
12
|
+
|
13
|
+
=== An example
|
14
|
+
require 'css_parser_master'
|
15
|
+
include CssParserMaster
|
16
|
+
|
17
|
+
parser = CssParserMaster::Parser.new
|
18
|
+
parser.load_uri!('http://example.com/styles/style.css')
|
19
|
+
|
20
|
+
# load a remote file, setting the base_uri and media_types
|
21
|
+
parser.load_uri!('../style.css', 'http://example.com/styles/inc/', [:screen, :handheld])
|
22
|
+
|
23
|
+
# load a local file, setting the base_dir and media_types
|
24
|
+
parser.load_file!('print.css', '~/styles/', :print)
|
25
|
+
|
26
|
+
# lookup a rule by a selector
|
27
|
+
parser.find('#content')
|
28
|
+
#=> 'font-size: 13px; line-height: 1.2;'
|
29
|
+
|
30
|
+
# lookup a rule by a selector and media type
|
31
|
+
parser.find('#content', [:screen, :handheld])
|
32
|
+
|
33
|
+
# iterate through selectors by media type
|
34
|
+
parser.each_selector(:screen) do |sel|
|
35
|
+
puts sel
|
36
|
+
puts sel.selector
|
37
|
+
puts sel.declarations
|
38
|
+
puts sel.specificity
|
39
|
+
...
|
40
|
+
end
|
41
|
+
|
42
|
+
# add a block of CSS
|
43
|
+
css = <<-EOT
|
44
|
+
body { margin: 0 1em; }
|
45
|
+
EOT
|
46
|
+
|
47
|
+
parser.add_block!(css)
|
48
|
+
|
49
|
+
# output all CSS rules in a single stylesheet
|
50
|
+
parser.to_s
|
51
|
+
=> #content { font-size: 13px; line-height: 1.2; }
|
52
|
+
body { margin: 0 1em; }
|
53
|
+
|
54
|
+
=== Testing
|
55
|
+
|
56
|
+
You can run the suite of unit tests using <tt>rake test</tt>.
|
57
|
+
|
58
|
+
The download/import tests use WEBrick. The tests set up
|
59
|
+
a temporary server on port 12000 and pull down files from the <tt>test/fixtures/</tt>
|
60
|
+
directory.
|
61
|
+
|
62
|
+
=== Design notes
|
63
|
+
|
64
|
+
The extensions that come with CssParserMaster were created in order to generate a more 'powerful' parse model.
|
65
|
+
This model should provide more powerful and intuitive ways of iterating and operating on the model.
|
66
|
+
Mainly Selector and Declaration were added as classes instead of the old version where they were simply hashes and strings.
|
67
|
+
|
68
|
+
=== Credits and code
|
69
|
+
|
70
|
+
Original CssParser by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-10.
|
71
|
+
CssParserMaster was created by Kristian Mandrup as an extension of CssParser in order to allow releases of the updated gem with some improvements.
|
72
|
+
|
73
|
+
Original project homepage: http://github.com/alexdunae/css_parser
|
74
|
+
|
75
|
+
Thanks to {Tyler Cunnion}[http://github.com/tylercunnion] for the updates leading to 1.0.0.
|
76
|
+
|
77
|
+
Made on Vancouver Island.
|
data/Rakefile.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gem|
|
4
|
+
gem.name = "css_parser_master"
|
5
|
+
gem.summary = %Q{Parse CSS files, access and operate on a model of the CSS rules}
|
6
|
+
gem.description = %Q{Parse a CSS file and access/operate on rulesets, selectors, declarations etc. Includes specificity calculated according to W3C spec.}
|
7
|
+
gem.email = "kmandrup@gmail.com"
|
8
|
+
gem.homepage = "http://github.com/kristianmandrup/load-me"
|
9
|
+
gem.authors = ["Kristian Mandrup", "Alex Dunae"]
|
10
|
+
# gem.add_development_dependency "rspec", ">= 2.0.0"
|
11
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
12
|
+
|
13
|
+
# add more gem options here
|
14
|
+
end
|
15
|
+
Jeweler::GemcutterTasks.new
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
18
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.2.4
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'zlib'
|
4
|
+
require 'iconv'
|
5
|
+
|
6
|
+
module CssParserMaster
|
7
|
+
VERSION = '1.2.5'
|
8
|
+
|
9
|
+
# Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
|
10
|
+
# (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
|
11
|
+
#
|
12
|
+
# Takes one or more RuleSet objects.
|
13
|
+
#
|
14
|
+
# Returns a RuleSet.
|
15
|
+
#
|
16
|
+
# ==== Cascading
|
17
|
+
# If a RuleSet object has its +specificity+ defined, that specificity is
|
18
|
+
# used in the cascade calculations.
|
19
|
+
#
|
20
|
+
# If no specificity is explicitly set and the RuleSet has *one* selector,
|
21
|
+
# the specificity is calculated using that selector.
|
22
|
+
#
|
23
|
+
# If no selectors or multiple selectors are present, the specificity is
|
24
|
+
# treated as 0.
|
25
|
+
#
|
26
|
+
# ==== Example #1
|
27
|
+
# rs1 = RuleSet.new(nil, 'color: black;')
|
28
|
+
# rs2 = RuleSet.new(nil, 'margin: 0px;')
|
29
|
+
#
|
30
|
+
# merged = CssParserMaster.merge(rs1, rs2)
|
31
|
+
#
|
32
|
+
# puts merged
|
33
|
+
# => "{ margin: 0px; color: black; }"
|
34
|
+
#
|
35
|
+
# ==== Example #2
|
36
|
+
# rs1 = RuleSet.new(nil, 'background-color: black;')
|
37
|
+
# rs2 = RuleSet.new(nil, 'background-image: none;')
|
38
|
+
#
|
39
|
+
# merged = CssParserMaster.merge(rs1, rs2)
|
40
|
+
#
|
41
|
+
# puts merged
|
42
|
+
# => "{ background: none black; }"
|
43
|
+
#--
|
44
|
+
# TODO: declaration_hashes should be able to contain a RuleSet
|
45
|
+
# this should be a Class method
|
46
|
+
def self.merge(*rule_sets)
|
47
|
+
@folded_declaration_cache = {}
|
48
|
+
|
49
|
+
# in case called like CssParser.merge([rule_set, rule_set])
|
50
|
+
rule_sets.flatten! if rule_sets[0].kind_of?(Array)
|
51
|
+
|
52
|
+
unless rule_sets.all? {|rs| rs.kind_of?(CssParser::RuleSet)}
|
53
|
+
raise ArgumentError, "all parameters must be CssParser::RuleSets."
|
54
|
+
end
|
55
|
+
|
56
|
+
return rule_sets[0] if rule_sets.length == 1
|
57
|
+
|
58
|
+
# Internal storage of CSS properties that we will keep
|
59
|
+
properties = {}
|
60
|
+
|
61
|
+
rule_sets.each do |rule_set|
|
62
|
+
rule_set.expand_shorthand!
|
63
|
+
|
64
|
+
specificity = rule_set.specificity
|
65
|
+
unless specificity
|
66
|
+
if rule_set.selectors.length == 1
|
67
|
+
specificity = calculate_specificity(rule_set.selectors[0])
|
68
|
+
else
|
69
|
+
specificity = 0
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
rule_set.each_declaration do |decl|
|
74
|
+
|
75
|
+
property = decl.property
|
76
|
+
value = decl.value
|
77
|
+
is_important = decl.important
|
78
|
+
|
79
|
+
# Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
|
80
|
+
if not properties.has_key?(decl.property) or
|
81
|
+
is_important or # step 2
|
82
|
+
properties[property][:specificity] < specificity or # step 3
|
83
|
+
properties[property][:specificity] == specificity # step 4
|
84
|
+
properties[property] = {:value => value, :specificity => specificity, :is_important => is_important}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
merged = RuleSet.new(nil, nil)
|
90
|
+
|
91
|
+
# TODO: what about important
|
92
|
+
properties.each do |property, details|
|
93
|
+
merged[property.strip] = details[:value].strip
|
94
|
+
end
|
95
|
+
|
96
|
+
merged.create_shorthand!
|
97
|
+
merged
|
98
|
+
end
|
99
|
+
|
100
|
+
# Calculates the specificity of a CSS selector
|
101
|
+
# per http://www.w3.org/TR/CSS21/cascade.html#specificity
|
102
|
+
#
|
103
|
+
# Returns an integer.
|
104
|
+
#
|
105
|
+
# ==== Example
|
106
|
+
# CssParser.calculate_specificity('#content div p:first-line a:link')
|
107
|
+
# => 114
|
108
|
+
#--
|
109
|
+
# Thanks to Rafael Salazar and Nick Fitzsimons on the css-discuss list for their help.
|
110
|
+
#++
|
111
|
+
def self.calculate_specificity(selector)
|
112
|
+
a = 0
|
113
|
+
b = selector.scan(/\#/).length
|
114
|
+
c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX).length
|
115
|
+
d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX).length
|
116
|
+
|
117
|
+
(a.to_s + b.to_s + c.to_s + d.to_s).to_i
|
118
|
+
rescue
|
119
|
+
return 0
|
120
|
+
end
|
121
|
+
|
122
|
+
# Make <tt>url()</tt> links absolute.
|
123
|
+
#
|
124
|
+
# Takes a block of CSS and returns it with all relative URIs converted to absolute URIs.
|
125
|
+
#
|
126
|
+
# "For CSS style sheets, the base URI is that of the style sheet, not that of the source document."
|
127
|
+
# per http://www.w3.org/TR/CSS21/syndata.html#uri
|
128
|
+
#
|
129
|
+
# Returns a string.
|
130
|
+
#
|
131
|
+
# ==== Example
|
132
|
+
# CssParser.convert_uris("body { background: url('../style/yellow.png?abc=123') };",
|
133
|
+
# "http://example.org/style/basic.css").inspect
|
134
|
+
# => "body { background: url('http://example.org/style/yellow.png?abc=123') };"
|
135
|
+
def self.convert_uris(css, base_uri)
|
136
|
+
out = ''
|
137
|
+
base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
|
138
|
+
|
139
|
+
out = css.gsub(URI_RX) do |s|
|
140
|
+
uri = $1.to_s
|
141
|
+
uri.gsub!(/["']+/, '')
|
142
|
+
# Don't process URLs that are already absolute
|
143
|
+
unless uri =~ /^[a-z]+\:\/\//i
|
144
|
+
begin
|
145
|
+
uri = base_uri.merge(uri)
|
146
|
+
rescue; end
|
147
|
+
end
|
148
|
+
"url('" + uri.to_s + "')"
|
149
|
+
end
|
150
|
+
out
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
require File.dirname(__FILE__) + '/css_parser_master/rule_set'
|
155
|
+
require File.dirname(__FILE__) + '/css_parser_master/regexps'
|
156
|
+
require File.dirname(__FILE__) + '/css_parser_master/parser'
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module CssParserMaster
|
2
|
+
class Declaration
|
3
|
+
attr_accessor :property, :value, :important, :order
|
4
|
+
|
5
|
+
def initialize(property, value, important = false, order = 0)
|
6
|
+
# puts "init new declaration: #{property}"
|
7
|
+
@property = property
|
8
|
+
@value = value
|
9
|
+
@important = important
|
10
|
+
@order = order
|
11
|
+
end
|
12
|
+
|
13
|
+
def [] index
|
14
|
+
case index
|
15
|
+
when :value
|
16
|
+
value
|
17
|
+
when :order
|
18
|
+
order
|
19
|
+
when :is_important
|
20
|
+
important
|
21
|
+
when :property
|
22
|
+
property
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_text(importance = nil)
|
27
|
+
"#{property}: #{value}#{ ' !important' if important || importance};"
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module CssParserMaster
|
2
|
+
module DeclarationAPI
|
3
|
+
|
4
|
+
def ensure_valid_declarations!
|
5
|
+
@declarations.each do |d|
|
6
|
+
name = d[0]
|
7
|
+
prop = d[1]
|
8
|
+
if prop.kind_of? Hash
|
9
|
+
value = prop[:value]
|
10
|
+
important = prop[:is_important]
|
11
|
+
@declarations[d[0]] = Declaration.new(name, value, important, @order += 1)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def each_declaration # :yields: property, value, is_important
|
18
|
+
ensure_valid_declarations!
|
19
|
+
decs = @declarations.sort { |a,b| a[1].order <=> b[1].order }
|
20
|
+
# puts "decs: #{decs.inspect}"
|
21
|
+
decs.each do |decl|
|
22
|
+
yield decl[1]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Return all declarations as a string.
|
28
|
+
#--
|
29
|
+
# TODO: Clean-up regexp doesn't seem to work
|
30
|
+
#++
|
31
|
+
def declarations_to_s(options = {})
|
32
|
+
options = {:force_important => false}.merge(options)
|
33
|
+
str = ''
|
34
|
+
importance = options[:force_important] # ? ' !important' : ''
|
35
|
+
self.each_declaration do |decl|
|
36
|
+
str += "#{decl.to_text(importance)}"
|
37
|
+
end
|
38
|
+
str.gsub(/^[\s]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Add a CSS declaration to the current RuleSet.
|
43
|
+
#
|
44
|
+
# rule_set.add_declaration!('color', 'blue')
|
45
|
+
#
|
46
|
+
# puts rule_set['color']
|
47
|
+
# => 'blue;'
|
48
|
+
#
|
49
|
+
# rule_set.add_declaration!('margin', '0px auto !important')
|
50
|
+
#
|
51
|
+
# puts rule_set['margin']
|
52
|
+
# => '0px auto !important;'
|
53
|
+
#
|
54
|
+
# If the property already exists its value will be over-written.
|
55
|
+
def add_declaration!(property, value)
|
56
|
+
if value.nil? or value.empty?
|
57
|
+
@declarations.delete(property)
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
value.gsub!(/;\Z/, '')
|
62
|
+
is_important = !value.gsub!(CssParserMaster::IMPORTANT_IN_PROPERTY_RX, '').nil?
|
63
|
+
property = property.downcase.strip
|
64
|
+
|
65
|
+
decl = CssParserMaster::Declaration.new property.downcase.strip, value.strip, is_important, @order += 1
|
66
|
+
# puts "new decl: #{decl.inspect}, #{decl.class}"
|
67
|
+
@declarations[property] = decl
|
68
|
+
end
|
69
|
+
alias_method :[]=, :add_declaration!
|
70
|
+
|
71
|
+
def parse_declarations!(block) # :nodoc:
|
72
|
+
@declarations ||= {}
|
73
|
+
|
74
|
+
return unless block
|
75
|
+
|
76
|
+
block.gsub!(/(^[\s]*)|([\s]*$)/, '')
|
77
|
+
|
78
|
+
decs = block.split(/[\;$]+/m)
|
79
|
+
|
80
|
+
decs.each do |decs|
|
81
|
+
if matches = decs.match(/(.[^:]*)\:(.[^;]*)(;|\Z)/i)
|
82
|
+
property, value, end_of_declaration = matches.captures
|
83
|
+
|
84
|
+
# puts "parse - property: #{property} , value: #{value}"
|
85
|
+
add_declaration!(property, value)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module CssParserMaster
|
2
|
+
class Declarations
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :declarations
|
6
|
+
|
7
|
+
def << declaration
|
8
|
+
declarations << declaration
|
9
|
+
end
|
10
|
+
|
11
|
+
def each
|
12
|
+
@declarations.each { |dec| yield dec }
|
13
|
+
end
|
14
|
+
|
15
|
+
def empty?
|
16
|
+
declarations.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(declarations)
|
20
|
+
@declarations = declarations
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|