stylesheet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +20 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +63 -0
- data/Rakefile +22 -0
- data/features/document_styles.feature +15 -0
- data/features/rule_declarations.feature +9 -0
- data/features/step_definitions/document_steps.rb +39 -0
- data/features/step_definitions/rule_declaration_steps.rb +13 -0
- data/features/step_definitions/style_rule_steps.rb +31 -0
- data/features/style_rules.feature +15 -0
- data/features/support/env.rb +6 -0
- data/lib/stylesheet.rb +35 -0
- data/lib/stylesheet/css_charset_rule.rb +24 -0
- data/lib/stylesheet/css_font_face_rule.rb +26 -0
- data/lib/stylesheet/css_import_rule.rb +41 -0
- data/lib/stylesheet/css_media_rule.rb +30 -0
- data/lib/stylesheet/css_rule.rb +57 -0
- data/lib/stylesheet/css_rule_list.rb +30 -0
- data/lib/stylesheet/css_style_declaration.rb +41 -0
- data/lib/stylesheet/css_style_rule.rb +29 -0
- data/lib/stylesheet/css_style_sheet.rb +100 -0
- data/lib/stylesheet/document.rb +63 -0
- data/lib/stylesheet/errors.rb +5 -0
- data/lib/stylesheet/inflector.rb +11 -0
- data/lib/stylesheet/location.rb +113 -0
- data/lib/stylesheet/media_list.rb +26 -0
- data/lib/stylesheet/request.rb +23 -0
- data/lib/stylesheet/style_sheet_list.rb +15 -0
- data/lib/stylesheet/version.rb +3 -0
- data/spec/css_charset_rule_spec.rb +39 -0
- data/spec/css_font_face_rule_spec.rb +47 -0
- data/spec/css_import_rule_spec.rb +178 -0
- data/spec/css_media_rule_spec.rb +57 -0
- data/spec/css_rule_list_spec.rb +74 -0
- data/spec/css_rule_spec.rb +102 -0
- data/spec/css_style_declaration_spec.rb +71 -0
- data/spec/css_style_rule_spec.rb +53 -0
- data/spec/css_style_sheet_spec.rb +157 -0
- data/spec/document_spec.rb +99 -0
- data/spec/fixtures/css/absolute_path.html +14 -0
- data/spec/fixtures/css/charset.html +13 -0
- data/spec/fixtures/css/font_face.html +13 -0
- data/spec/fixtures/css/full_url.html +14 -0
- data/spec/fixtures/css/html4.html +15 -0
- data/spec/fixtures/css/html5.html +14 -0
- data/spec/fixtures/css/inline.html +33 -0
- data/spec/fixtures/css/inline_import.html +15 -0
- data/spec/fixtures/css/invalid.html +14 -0
- data/spec/fixtures/css/media.html +13 -0
- data/spec/fixtures/css/relative_path.html +14 -0
- data/spec/fixtures/css/stylesheets/charset.css +5 -0
- data/spec/fixtures/css/stylesheets/colors.css +0 -0
- data/spec/fixtures/css/stylesheets/font_face.css +6 -0
- data/spec/fixtures/css/stylesheets/fonts.css +0 -0
- data/spec/fixtures/css/stylesheets/media.css +3 -0
- data/spec/fixtures/css/stylesheets/print.css +3 -0
- data/spec/fixtures/css/stylesheets/screen.css +16 -0
- data/spec/fixtures/css_import/index.html +14 -0
- data/spec/fixtures/css_import/stylesheets/import1.css +3 -0
- data/spec/fixtures/css_import/stylesheets/import2.css +3 -0
- data/spec/fixtures/css_import/stylesheets/import3.css +3 -0
- data/spec/fixtures/css_import/stylesheets/import4.css +3 -0
- data/spec/fixtures/css_import/stylesheets/import5.css +3 -0
- data/spec/fixtures/css_import/stylesheets/import6.css +3 -0
- data/spec/fixtures/css_import/stylesheets/import7.css +3 -0
- data/spec/fixtures/css_import/stylesheets/import8.css +3 -0
- data/spec/fixtures/css_import/stylesheets/import9.css +3 -0
- data/spec/fixtures/css_import/stylesheets/print.css +3 -0
- data/spec/fixtures/css_import/stylesheets/screen.css +15 -0
- data/spec/fixtures/fonts/VeraSeBd.ttf +0 -0
- data/spec/inflector_spec.rb +17 -0
- data/spec/location_spec.rb +260 -0
- data/spec/media_list_spec.rb +108 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/stubs/fake_request.rb +19 -0
- data/spec/style_sheet_list_spec.rb +53 -0
- data/spec/version_spec.rb +9 -0
- data/stylesheet.gemspec +34 -0
- metadata +294 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module Stylesheet
|
2
|
+
class CssRule
|
3
|
+
STYLE_RULE = 1
|
4
|
+
CHARSET_RULE = 2
|
5
|
+
IMPORT_RULE = 3
|
6
|
+
MEDIA_RULE = 4
|
7
|
+
FONT_FACE_RULE = 5
|
8
|
+
|
9
|
+
attr_writer :type
|
10
|
+
attr_reader :parent_style_sheet, :parent_rule, :css_text
|
11
|
+
|
12
|
+
|
13
|
+
# keep track of subclasses for factory
|
14
|
+
class << self
|
15
|
+
attr_reader :rule_classes
|
16
|
+
end
|
17
|
+
|
18
|
+
@rule_classes = []
|
19
|
+
|
20
|
+
def self.inherited(subclass)
|
21
|
+
CssRule.rule_classes << subclass
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.factory(args)
|
25
|
+
rule = CssRule.rule_classes.find do |klass|
|
26
|
+
klass.matches_rule?(args[:css_text])
|
27
|
+
end
|
28
|
+
rule.new(args) if rule
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def initialize(args)
|
33
|
+
@parent_style_sheet = args[:parent_style_sheet]
|
34
|
+
@parent_rule = args[:parent_rule]
|
35
|
+
@css_text = args[:css_text]
|
36
|
+
parse_css_text
|
37
|
+
end
|
38
|
+
|
39
|
+
def type
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
def matches_rule?
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
"#<#{self.class.name} css_text:#{css_text}>"
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def parse_css_text(css_text)
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Stylesheet
|
2
|
+
class CssRuleList
|
3
|
+
extend Forwardable
|
4
|
+
def_delegators :@rules, :length, :size, :[], :each, :to_s
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(rules, parent = nil)
|
8
|
+
@rules = parse(rules, parent)
|
9
|
+
end
|
10
|
+
|
11
|
+
def item(index)
|
12
|
+
@rules[index]
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def parse(rules, parent)
|
18
|
+
# clean extraneous whitespace
|
19
|
+
rules = rules.to_s.gsub(/\s+/m, " ").gsub(/([\};])\s/, '\1')
|
20
|
+
|
21
|
+
directive_re = "@.+?;"
|
22
|
+
rules_re = ".+?\{.+?\}"
|
23
|
+
split_rules = rules.scan(/(#{directive_re}|#{rules_re})/im).map {|r| r[0] }
|
24
|
+
|
25
|
+
split_rules.map do |css_text|
|
26
|
+
CssRule.factory(:css_text => css_text, :parent_style_sheet => parent)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Stylesheet
|
2
|
+
class CssStyleDeclaration
|
3
|
+
extend Forwardable
|
4
|
+
def_delegators :@declarations, :length, :size, :[], :each, :<<, :push, :delete, :to_s
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :parent_rule
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
@parent_rule = options[:parent_rule]
|
11
|
+
self.css_text = options[:css_text]
|
12
|
+
end
|
13
|
+
|
14
|
+
def css_text=(css_text)
|
15
|
+
@declarations, @rules = [], Hash.new("")
|
16
|
+
|
17
|
+
css_text.to_s.strip.chomp(";").split(";").each do |declaration|
|
18
|
+
property, value = declaration.split(":", 2)
|
19
|
+
@declarations << declaration.strip
|
20
|
+
@rules[Inflector.camelize(property.strip)] = parse_value(value.strip)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def css_text
|
25
|
+
css_text = @declarations.join("; ")
|
26
|
+
css_text += ";" if css_text != ""
|
27
|
+
end
|
28
|
+
|
29
|
+
alias_method :to_s, :css_text
|
30
|
+
|
31
|
+
def method_missing(name, *args)
|
32
|
+
@rules[name.to_s]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def parse_value(value)
|
38
|
+
value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Stylesheet
|
2
|
+
class CssStyleRule < CssRule
|
3
|
+
|
4
|
+
attr_reader :selector_text
|
5
|
+
|
6
|
+
def type
|
7
|
+
CssRule::STYLE_RULE
|
8
|
+
end
|
9
|
+
|
10
|
+
def style
|
11
|
+
CssStyleDeclaration.new(:css_text => @declarations,
|
12
|
+
:parent_rule => self)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.matches_rule?(text)
|
16
|
+
!text.include?("@")
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def parse_css_text
|
22
|
+
return unless css_text.include?("{")
|
23
|
+
|
24
|
+
selector, declarations = css_text.split("{")
|
25
|
+
@selector_text = selector.strip
|
26
|
+
@declarations = declarations.gsub(/\}\s*$/, "")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Stylesheet
|
2
|
+
class CssStyleSheet
|
3
|
+
attr_accessor :parent, :title
|
4
|
+
attr_reader :href, :media
|
5
|
+
attr_writer :disabled, :type
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
if args.kind_of?(String)
|
9
|
+
init_with_url(args)
|
10
|
+
else
|
11
|
+
init_with_hash(args)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def init_with_url(url)
|
16
|
+
self.href = url
|
17
|
+
self.media = ""
|
18
|
+
self.content = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def init_with_hash(args)
|
22
|
+
@parent = args[:parent]
|
23
|
+
@title = args[:title]
|
24
|
+
@type = args[:type]
|
25
|
+
|
26
|
+
self.href = args[:href]
|
27
|
+
self.location = args[:location]
|
28
|
+
self.media = args[:media]
|
29
|
+
self.content = args[:content]
|
30
|
+
end
|
31
|
+
|
32
|
+
def disabled?
|
33
|
+
@disabled || false
|
34
|
+
end
|
35
|
+
|
36
|
+
def type
|
37
|
+
@type || "text/css"
|
38
|
+
end
|
39
|
+
|
40
|
+
def href=(url)
|
41
|
+
return unless url
|
42
|
+
@url = url
|
43
|
+
@href = location.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def media=(media)
|
47
|
+
@media ||= MediaList.new(media)
|
48
|
+
end
|
49
|
+
|
50
|
+
def content=(content)
|
51
|
+
@content = content if content != ""
|
52
|
+
end
|
53
|
+
|
54
|
+
def content
|
55
|
+
@content ||= request_content
|
56
|
+
end
|
57
|
+
|
58
|
+
def css_rules
|
59
|
+
@css_rules ||= CssRuleList.new(content, self)
|
60
|
+
end
|
61
|
+
|
62
|
+
alias_method :rules, :css_rules
|
63
|
+
|
64
|
+
def parent_style_sheet
|
65
|
+
parent
|
66
|
+
end
|
67
|
+
|
68
|
+
def location
|
69
|
+
return if inline_css?
|
70
|
+
@location ||= Location.new(@url, parent && parent.location)
|
71
|
+
end
|
72
|
+
|
73
|
+
def location=(location)
|
74
|
+
return unless location
|
75
|
+
|
76
|
+
@location = location
|
77
|
+
@url = location.href
|
78
|
+
@href = location.href
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def standalone_css?
|
84
|
+
!parent
|
85
|
+
end
|
86
|
+
|
87
|
+
def inline_css?
|
88
|
+
!@url || @url == ""
|
89
|
+
end
|
90
|
+
|
91
|
+
def request_content
|
92
|
+
raise InvalidLocationError unless location.valid?
|
93
|
+
request.get(location.href)
|
94
|
+
end
|
95
|
+
|
96
|
+
def request
|
97
|
+
@request ||= Stylesheet.request
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Stylesheet
|
2
|
+
class Document
|
3
|
+
|
4
|
+
def initialize(url=nil)
|
5
|
+
@url = url
|
6
|
+
end
|
7
|
+
|
8
|
+
def location
|
9
|
+
@location ||= Location.new(@url)
|
10
|
+
end
|
11
|
+
|
12
|
+
def location=(location)
|
13
|
+
@location = location
|
14
|
+
end
|
15
|
+
|
16
|
+
def text
|
17
|
+
@text ||= request_content
|
18
|
+
end
|
19
|
+
|
20
|
+
def style_sheets
|
21
|
+
@style_sheets ||= StyleSheetList.new(styles)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
"#<Document location:#{location.href}>"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def styles
|
31
|
+
(inline_styles + external_styles).map do |style|
|
32
|
+
{ parent: self,
|
33
|
+
content: style.inner_html,
|
34
|
+
href: style["href"],
|
35
|
+
media: style["media"],
|
36
|
+
title: style["title"] }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# find all inline styles and build new stylesheet from them
|
41
|
+
def inline_styles
|
42
|
+
parser.css("style").to_a
|
43
|
+
end
|
44
|
+
|
45
|
+
def external_styles
|
46
|
+
parser.css("link[rel='stylesheet']").to_a
|
47
|
+
end
|
48
|
+
|
49
|
+
def request_content
|
50
|
+
raise InvalidLocationError unless location.valid?
|
51
|
+
|
52
|
+
request.get(location.href)
|
53
|
+
end
|
54
|
+
|
55
|
+
def parser
|
56
|
+
@parser ||= Nokogiri::HTML(text)
|
57
|
+
end
|
58
|
+
|
59
|
+
def request
|
60
|
+
@request ||= Stylesheet.request
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Stylesheet
|
2
|
+
class Location
|
3
|
+
attr_accessor :uri, :host, :href
|
4
|
+
attr_reader :hash, :pathname, :port, :protocol, :search, :parent
|
5
|
+
|
6
|
+
alias_method :hostname, :host
|
7
|
+
alias_method :hostname=, :host=
|
8
|
+
|
9
|
+
def initialize(url, parent = nil)
|
10
|
+
@uri = parse_uri(url)
|
11
|
+
@host = uri.host
|
12
|
+
@parent = parent
|
13
|
+
|
14
|
+
self.init_from_uri
|
15
|
+
self.expand_paths_from_parent
|
16
|
+
end
|
17
|
+
|
18
|
+
def init_from_uri
|
19
|
+
self.protocol = uri.scheme
|
20
|
+
self.pathname = uri.path
|
21
|
+
self.port = uri.port
|
22
|
+
self.search = uri.query
|
23
|
+
self.hash = uri.fragment
|
24
|
+
end
|
25
|
+
|
26
|
+
def protocol=(protocol)
|
27
|
+
protocol = protocol.to_s.gsub(":", "")
|
28
|
+
@protocol = "#{protocol}:"
|
29
|
+
end
|
30
|
+
|
31
|
+
def pathname=(pathname)
|
32
|
+
@pathname = pathname && pathname[0,1] == "/" ? pathname : "/#{pathname}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def port=(port)
|
36
|
+
@port = standard_port? ? "" : "#{port}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def search=(search)
|
40
|
+
search = search.to_s.gsub("?", "")
|
41
|
+
@search = search && search != "" ? "?#{search}" : ""
|
42
|
+
end
|
43
|
+
|
44
|
+
def hash=(hash)
|
45
|
+
hash = hash.to_s.gsub("#", "")
|
46
|
+
@hash = hash && hash != "" ? "##{hash}" : ""
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid?
|
50
|
+
valid_protocol? && valid_host?
|
51
|
+
end
|
52
|
+
|
53
|
+
def expand_paths_from_parent
|
54
|
+
return if valid_protocol?
|
55
|
+
return unless parent
|
56
|
+
|
57
|
+
self.pathname = URI.join(parent.to_s, uri.path).path
|
58
|
+
self.protocol = parent.protocol
|
59
|
+
self.host = parent.host
|
60
|
+
end
|
61
|
+
|
62
|
+
def href
|
63
|
+
port_w_colon = valid_port? ? ":#{port}" : ""
|
64
|
+
scheme = valid_protocol? ? "#{protocol}//" : ""
|
65
|
+
|
66
|
+
"#{scheme}#{host}#{port_w_colon}#{pathname}#{search}#{hash}"
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :to_s, :href
|
70
|
+
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def valid_protocol?
|
75
|
+
protocol && protocol != ":"
|
76
|
+
end
|
77
|
+
|
78
|
+
def valid_host?
|
79
|
+
host && host != ""
|
80
|
+
end
|
81
|
+
|
82
|
+
def valid_port?
|
83
|
+
port && port != ""
|
84
|
+
end
|
85
|
+
|
86
|
+
def standard_port?
|
87
|
+
port_80? || port_443?
|
88
|
+
end
|
89
|
+
|
90
|
+
def port_80?
|
91
|
+
uri && uri.port == 80 && uri.scheme == "http"
|
92
|
+
end
|
93
|
+
|
94
|
+
def port_443?
|
95
|
+
uri && uri.port == 443 && uri.scheme == "https"
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse_uri(url)
|
99
|
+
uri = begin
|
100
|
+
URI.parse(url.strip)
|
101
|
+
rescue URI::InvalidURIError
|
102
|
+
URI.parse(URI.escape(url.strip))
|
103
|
+
end
|
104
|
+
|
105
|
+
# re-raise external library errors in our namespace
|
106
|
+
rescue URI::InvalidURIError => error
|
107
|
+
raise Stylesheet::InvalidLocationError.new(
|
108
|
+
"#{error.class}: #{error.message}")
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|