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
|