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.
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
+