stylesheet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/.gitignore +20 -0
  2. data/Gemfile +4 -0
  3. data/Guardfile +9 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +63 -0
  6. data/Rakefile +22 -0
  7. data/features/document_styles.feature +15 -0
  8. data/features/rule_declarations.feature +9 -0
  9. data/features/step_definitions/document_steps.rb +39 -0
  10. data/features/step_definitions/rule_declaration_steps.rb +13 -0
  11. data/features/step_definitions/style_rule_steps.rb +31 -0
  12. data/features/style_rules.feature +15 -0
  13. data/features/support/env.rb +6 -0
  14. data/lib/stylesheet.rb +35 -0
  15. data/lib/stylesheet/css_charset_rule.rb +24 -0
  16. data/lib/stylesheet/css_font_face_rule.rb +26 -0
  17. data/lib/stylesheet/css_import_rule.rb +41 -0
  18. data/lib/stylesheet/css_media_rule.rb +30 -0
  19. data/lib/stylesheet/css_rule.rb +57 -0
  20. data/lib/stylesheet/css_rule_list.rb +30 -0
  21. data/lib/stylesheet/css_style_declaration.rb +41 -0
  22. data/lib/stylesheet/css_style_rule.rb +29 -0
  23. data/lib/stylesheet/css_style_sheet.rb +100 -0
  24. data/lib/stylesheet/document.rb +63 -0
  25. data/lib/stylesheet/errors.rb +5 -0
  26. data/lib/stylesheet/inflector.rb +11 -0
  27. data/lib/stylesheet/location.rb +113 -0
  28. data/lib/stylesheet/media_list.rb +26 -0
  29. data/lib/stylesheet/request.rb +23 -0
  30. data/lib/stylesheet/style_sheet_list.rb +15 -0
  31. data/lib/stylesheet/version.rb +3 -0
  32. data/spec/css_charset_rule_spec.rb +39 -0
  33. data/spec/css_font_face_rule_spec.rb +47 -0
  34. data/spec/css_import_rule_spec.rb +178 -0
  35. data/spec/css_media_rule_spec.rb +57 -0
  36. data/spec/css_rule_list_spec.rb +74 -0
  37. data/spec/css_rule_spec.rb +102 -0
  38. data/spec/css_style_declaration_spec.rb +71 -0
  39. data/spec/css_style_rule_spec.rb +53 -0
  40. data/spec/css_style_sheet_spec.rb +157 -0
  41. data/spec/document_spec.rb +99 -0
  42. data/spec/fixtures/css/absolute_path.html +14 -0
  43. data/spec/fixtures/css/charset.html +13 -0
  44. data/spec/fixtures/css/font_face.html +13 -0
  45. data/spec/fixtures/css/full_url.html +14 -0
  46. data/spec/fixtures/css/html4.html +15 -0
  47. data/spec/fixtures/css/html5.html +14 -0
  48. data/spec/fixtures/css/inline.html +33 -0
  49. data/spec/fixtures/css/inline_import.html +15 -0
  50. data/spec/fixtures/css/invalid.html +14 -0
  51. data/spec/fixtures/css/media.html +13 -0
  52. data/spec/fixtures/css/relative_path.html +14 -0
  53. data/spec/fixtures/css/stylesheets/charset.css +5 -0
  54. data/spec/fixtures/css/stylesheets/colors.css +0 -0
  55. data/spec/fixtures/css/stylesheets/font_face.css +6 -0
  56. data/spec/fixtures/css/stylesheets/fonts.css +0 -0
  57. data/spec/fixtures/css/stylesheets/media.css +3 -0
  58. data/spec/fixtures/css/stylesheets/print.css +3 -0
  59. data/spec/fixtures/css/stylesheets/screen.css +16 -0
  60. data/spec/fixtures/css_import/index.html +14 -0
  61. data/spec/fixtures/css_import/stylesheets/import1.css +3 -0
  62. data/spec/fixtures/css_import/stylesheets/import2.css +3 -0
  63. data/spec/fixtures/css_import/stylesheets/import3.css +3 -0
  64. data/spec/fixtures/css_import/stylesheets/import4.css +3 -0
  65. data/spec/fixtures/css_import/stylesheets/import5.css +3 -0
  66. data/spec/fixtures/css_import/stylesheets/import6.css +3 -0
  67. data/spec/fixtures/css_import/stylesheets/import7.css +3 -0
  68. data/spec/fixtures/css_import/stylesheets/import8.css +3 -0
  69. data/spec/fixtures/css_import/stylesheets/import9.css +3 -0
  70. data/spec/fixtures/css_import/stylesheets/print.css +3 -0
  71. data/spec/fixtures/css_import/stylesheets/screen.css +15 -0
  72. data/spec/fixtures/fonts/VeraSeBd.ttf +0 -0
  73. data/spec/inflector_spec.rb +17 -0
  74. data/spec/location_spec.rb +260 -0
  75. data/spec/media_list_spec.rb +108 -0
  76. data/spec/spec_helper.rb +10 -0
  77. data/spec/stubs/fake_request.rb +19 -0
  78. data/spec/style_sheet_list_spec.rb +53 -0
  79. data/spec/version_spec.rb +9 -0
  80. data/stylesheet.gemspec +34 -0
  81. 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,5 @@
1
+ module Stylesheet
2
+ class Error < StandardError; end
3
+ class InvalidLocationError < Error; end
4
+ class NotImplementedError < Error; end
5
+ end
@@ -0,0 +1,11 @@
1
+ module Stylesheet
2
+ class Inflector
3
+ def self.camelize(string)
4
+ string.gsub(/(-)(.)/) {|m| m[1].upcase }
5
+ end
6
+
7
+ def self.dasherize(string)
8
+ string.gsub(/(.)([A-Z])/) {|m| "#{m[0]}-#{m[1].downcase}" }
9
+ end
10
+ end
11
+ 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
+