PageRenderTime 39ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/css_parser.rb

http://ruby-css-parser.googlecode.com/
Ruby | 149 lines | 69 code | 15 blank | 65 comment | 7 complexity | dc9d7c9fe7fcd256e15786b5ece7ca18 MD5 | raw file
  1. $:.unshift File.dirname(__FILE__)
  2. require 'uri'
  3. require 'md5'
  4. require 'zlib'
  5. require 'iconv'
  6. require 'css_parser/rule_set'
  7. require 'css_parser/regexps'
  8. require 'css_parser/parser'
  9. module CssParser
  10. # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
  11. # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
  12. #
  13. # Takes one or more RuleSet objects.
  14. #
  15. # Returns a RuleSet.
  16. #
  17. # ==== Cascading
  18. # If a RuleSet object has its +specificity+ defined, that specificity is
  19. # used in the cascade calculations.
  20. #
  21. # If no specificity is explicitly set and the RuleSet has *one* selector,
  22. # the specificity is calculated using that selector.
  23. #
  24. # If no selectors or multiple selectors are present, the specificity is
  25. # treated as 0.
  26. #
  27. # ==== Example #1
  28. # rs1 = RuleSet.new(nil, 'color: black;')
  29. # rs2 = RuleSet.new(nil, 'margin: 0px;')
  30. #
  31. # merged = CssParser.merge(rs1, rs2)
  32. #
  33. # puts merged
  34. # => "{ margin: 0px; color: black; }"
  35. #
  36. # ==== Example #2
  37. # rs1 = RuleSet.new(nil, 'background-color: black;')
  38. # rs2 = RuleSet.new(nil, 'background-image: none;')
  39. #
  40. # merged = CssParser.merge(rs1, rs2)
  41. #
  42. # puts merged
  43. # => "{ background: none black; }"
  44. #--
  45. # TODO: declaration_hashes should be able to contain a RuleSet
  46. # this should be a Class method
  47. def CssParser.merge(*rule_sets)
  48. @folded_declaration_cache = {}
  49. # in case called like CssParser.merge([rule_set, rule_set])
  50. rule_sets.flatten! if rule_sets[0].kind_of?(Array)
  51. unless rule_sets.all? {|rs| rs.kind_of?(CssParser::RuleSet)}
  52. raise ArgumentError, "all parameters must be CssParser::RuleSets."
  53. end
  54. return rule_sets[0] if rule_sets.length == 1
  55. # Internal storage of CSS properties that we will keep
  56. properties = {}
  57. rule_sets.each do |rule_set|
  58. rule_set.expand_shorthand!
  59. specificity = rule_set.specificity
  60. unless specificity
  61. if rule_set.selectors.length == 1
  62. specificity = calculate_specificity(rule_set.selectors[0])
  63. else
  64. specificity = 0
  65. end
  66. end
  67. rule_set.each_declaration do |property, value, is_important|
  68. # Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
  69. if not properties.has_key?(property) or
  70. is_important or # step 2
  71. properties[property][:specificity] < specificity or # step 3
  72. properties[property][:specificity] == specificity # step 4
  73. properties[property] = {:value => value, :specificity => specificity, :is_important => is_important}
  74. end
  75. end
  76. end
  77. merged = RuleSet.new(nil, nil)
  78. # TODO: what about important
  79. properties.each do |property, details|
  80. merged[property.strip] = details[:value].strip
  81. end
  82. merged.create_shorthand!
  83. merged
  84. end
  85. # Calculates the specificity of a CSS selector
  86. # per http://www.w3.org/TR/CSS21/cascade.html#specificity
  87. #
  88. # Returns an integer.
  89. #
  90. # ==== Example
  91. # CssParser.calculate_specificity('#content div p:first-line a:link')
  92. # => 114
  93. #--
  94. # Thanks to Rafael Salazar and Nick Fitzsimons on the css-discuss list for their help.
  95. #++
  96. def CssParser.calculate_specificity(selector)
  97. a = 0
  98. b = selector.scan(/\#/).length
  99. c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX).length
  100. d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX).length
  101. (a.to_s + b.to_s + c.to_s + d.to_s).to_i
  102. rescue
  103. return 0
  104. end
  105. # Make <tt>url()</tt> links absolute.
  106. #
  107. # Takes a block of CSS and returns it with all relative URIs converted to absolute URIs.
  108. #
  109. # "For CSS style sheets, the base URI is that of the style sheet, not that of the source document."
  110. # per http://www.w3.org/TR/CSS21/syndata.html#uri
  111. #
  112. # Returns a string.
  113. #
  114. # ==== Example
  115. # CssParser.convert_uris("body { background: url('../style/yellow.png?abc=123') };",
  116. # "http://example.org/style/basic.css").inspect
  117. # => "body { background: url('http://example.org/style/yellow.png?abc=123') };"
  118. def self.convert_uris(css, base_uri)
  119. out = ''
  120. base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
  121. out = css.gsub(URI_RX) do |s|
  122. uri = $1.to_s
  123. uri.gsub!(/["']+/, '')
  124. # Don't process URLs that are already absolute
  125. unless uri =~ /^[a-z]+\:\/\//i
  126. begin
  127. uri = base_uri.merge(uri)
  128. rescue; end
  129. end
  130. "url('" + uri.to_s + "')"
  131. end
  132. out
  133. end
  134. end