File: css_parser.rb

package info (click to toggle)
ruby-css-parser 1.21.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 388 kB
  • sloc: ruby: 3,118; makefile: 6
file content (158 lines) | stat: -rw-r--r-- 5,084 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# frozen_string_literal: true

require 'addressable/uri'
require 'uri'
require 'net/https'
require 'digest/md5'
require 'zlib'
require 'stringio'

require 'css_parser/version'
require 'css_parser/rule_set'
require 'css_parser/regexps'
require 'css_parser/parser'

module CssParser
  # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
  # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
  #
  # Takes one or more RuleSet objects.
  #
  # Returns a RuleSet.
  #
  # ==== Cascading
  # If a RuleSet object has its +specificity+ defined, that specificity is
  # used in the cascade calculations.
  #
  # If no specificity is explicitly set and the RuleSet has *one* selector,
  # the specificity is calculated using that selector.
  #
  # If no selectors the specificity is treated as 0.
  #
  # If multiple selectors are present then the greatest specificity is used.
  #
  # ==== Example #1
  #   rs1 = RuleSet.new(nil, 'color: black;')
  #   rs2 = RuleSet.new(nil, 'margin: 0px;')
  #
  #   merged = CssParser.merge(rs1, rs2)
  #
  #   puts merged
  #   => "{ margin: 0px; color: black; }"
  #
  # ==== Example #2
  #   rs1 = RuleSet.new(nil, 'background-color: black;')
  #   rs2 = RuleSet.new(nil, 'background-image: none;')
  #
  #   merged = CssParser.merge(rs1, rs2)
  #
  #   puts merged
  #   => "{ background: none black; }"
  #--
  # TODO: declaration_hashes should be able to contain a RuleSet
  #       this should be a Class method
  def self.merge(*rule_sets)
    @folded_declaration_cache = {}

    # in case called like CssParser.merge([rule_set, rule_set])
    rule_sets.flatten! if rule_sets[0].is_a?(Array)

    unless rule_sets.all?(CssParser::RuleSet)
      raise ArgumentError, 'all parameters must be CssParser::RuleSets.'
    end

    return rule_sets[0] if rule_sets.length == 1

    # Internal storage of CSS properties that we will keep
    properties = {}

    rule_sets.each do |rule_set|
      rule_set.expand_shorthand!

      specificity = rule_set.specificity
      specificity ||= rule_set.selectors.filter_map { |s| calculate_specificity(s) }.max || 0

      rule_set.each_declaration do |property, value, is_important|
        # Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
        if !properties.key?(property)
          properties[property] = {value: value, specificity: specificity, is_important: is_important}
        elsif is_important
          if !properties[property][:is_important] || properties[property][:specificity] <= specificity
            properties[property] = {value: value, specificity: specificity, is_important: is_important}
          end
        elsif properties[property][:specificity] < specificity || properties[property][:specificity] == specificity
          unless properties[property][:is_important]
            properties[property] = {value: value, specificity: specificity, is_important: is_important}
          end
        end
      end
    end

    merged = properties.each_with_object(RuleSet.new(nil, nil)) do |(property, details), rule_set|
      value = details[:value].strip
      rule_set[property.strip] = details[:is_important] ? "#{value.gsub(/;\Z/, '')}!important" : value
    end

    merged.create_shorthand!
    merged
  end

  # Calculates the specificity of a CSS selector
  # per http://www.w3.org/TR/CSS21/cascade.html#specificity
  #
  # Returns an integer.
  #
  # ==== Example
  #  CssParser.calculate_specificity('#content div p:first-line a:link')
  #  => 114
  #--
  # Thanks to Rafael Salazar and Nick Fitzsimons on the css-discuss list for their help.
  #++
  def self.calculate_specificity(selector)
    a = 0
    b = selector.scan('#').length
    c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC).length
    d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC).length

    "#{a}#{b}#{c}#{d}".to_i
  rescue
    0
  end

  # Make <tt>url()</tt> links absolute.
  #
  # Takes a block of CSS and returns it with all relative URIs converted to absolute URIs.
  #
  # "For CSS style sheets, the base URI is that of the style sheet, not that of the source document."
  # per http://www.w3.org/TR/CSS21/syndata.html#uri
  #
  # Returns a string.
  #
  # ==== Example
  #  CssParser.convert_uris("body { background: url('../style/yellow.png?abc=123') };",
  #               "http://example.org/style/basic.css").inspect
  #  => "body { background: url('http://example.org/style/yellow.png?abc=123') };"
  def self.convert_uris(css, base_uri)
    base_uri = Addressable::URI.parse(base_uri) unless base_uri.is_a?(Addressable::URI)

    css.gsub(URI_RX) do
      uri = Regexp.last_match(1).to_s.gsub(/["']+/, '')
      # Don't process URLs that are already absolute
      unless uri.match?(%r{^[a-z]+://}i)
        begin
          uri = base_uri.join(uri)
        rescue
          nil
        end
      end
      "url('#{uri}')"
    end
  end

  def self.sanitize_media_query(raw)
    mq = raw.to_s.gsub(/\s+/, ' ')
    mq.strip!
    mq = 'all' if mq.empty?
    mq.to_sym
  end
end