css-parser-1.3.6/0000755000175000017500000000000012542615072014021 5ustar terceiroterceirocss-parser-1.3.6/CHANGELOG.md0000644000175000017500000000620212542615072015632 0ustar terceiroterceiro## Ruby CSS Parser CHANGELOG ### Version 1.3.6 * Fix bug not setting general rules after media query @jievans. * We doesn't support Ruby 1.8 anymore. * Run tests on Ruby 2.0 and Ruby 2.1. * Respect the :import option. ### Version 1.3.5 * Use URI#request_uri instead of URI#path @duckinator. * Media_query_support @mzsanford * Don't require open-uri @aripollak * Symbols not sortable on 1.8.7 @morten * Improve create_dimensions_shorthand performance @aaronjensen * Fixes hash ordering in tests @morten ### Version 1.3.4 * Enable code highlighting for tests @grosser * Fix error in media query parsing @smgt * Add test to missing cleaning of media type in parsing @smgt ### Version 1.3.3 * Require version before requiring classes that depend on it @morten ### Version 1.3.2 * Fix them crazy requires and only define version once @grosser * Apply ocd @grosser ### Version 1.3.1 * More tests (and fixes) for background gradients @fortnightlabs * Support declarations with `;` in them @flavorpill * Stricter detection of !important @flavorpill ### Version 1.3.0 * Updates of gem by @grosser * Multiple selectors should properly calculate specificity @alexdunae * Specificity: The selector with the highest specificity may be in a compound selector statement? @morten * Selectors should not be registered with surrounding whitespace. @morten * Fix RE_GRADIENT reference @alexdunae * Add load_string! method tests @alexdunae * Gradient regexp tests @alexdunae * Edited rule set @mccuskk ### Version 1.2.6 * JRuby and Ruby 1.9.3-preview1 compat ### Version 1.2.5 * Fix merging of multiple !important rules to match the spec ### Version 1.2.3 * First pass of media query support ### Version 1.2.2 * Fix merging of multiple !important rules to match the spec ### Version 1.2.1 * Better border shorthand handling * List shorthand handling * Malformed URI handling improvements * Use Bundler ### Version 1.2.0 * Specificity improvements * RGBA, HSL and HSLA support * Bug fixes ### Version 1.1.9 * Add remove_declaration! to RuleSet ### Version 1.1.8 * Fix syntax error ### Version 1.1.7 * Automatically close missing braces at the end of a block ### Version 1.1.6 * Fix media type handling in add_block! and load_uri! ### Version 1.1.5 * Fix merging of !important declarations ### Version 1.1.4 * Ruby 1.9.2 compat ### Version 1.1.3 * allow limiting by media type in add_block! ### Version 1.1.2 * improve parsing of malformed declarations * improve support for local files * added support for loading over SSL * added support for deflate ### Version 1.1.1 * Ruby 1.9 compatibility * @import regexp updates * various bug fixes ### Version 1.1.0 * Added support for local @import * Better remote @import handling ### Version 1.0.1 * Fallback for declarations without sort order ### Version 1.0.0 * Various test fixes and udpate for Ruby 1.9 (thanks to Tyler Cunnion) * Allow setting CSS declarations to nil ### Version 0.9 * Initial version forked from Premailer project ### TODO: Future * re-implement caching on CssParser.merge * correctly parse http://www.webstandards.org/files/acid2/test.html css-parser-1.3.6/.travis.yml0000644000175000017500000000012712542615072016132 0ustar terceiroterceironotifications: disabled: true rvm: - 1.9.2 - 1.9.3 - 2.0.0 - 2.1.0 - jruby css-parser-1.3.6/lib/0000755000175000017500000000000012542615072014567 5ustar terceiroterceirocss-parser-1.3.6/lib/css_parser/0000755000175000017500000000000012542615072016733 5ustar terceiroterceirocss-parser-1.3.6/lib/css_parser/regexps.rb0000644000175000017500000001114212542615072020734 0ustar terceiroterceiromodule CssParser def self.regex_possible_values *values Regexp.new("([\s]*^)?(#{values.join('|')})([\s]*$)?", 'i') end # :stopdoc: # Base types RE_NL = Regexp.new('(\n|\r\n|\r|\f)') RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') #[^\0-\177] RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n') RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])') RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE, 'n') # General strings RE_STRING1 = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")') RE_STRING2 = Regexp.new('(\'(.[^\n\r\f\\\']*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\')') RE_STRING = Regexp.union(RE_STRING1, RE_STRING2) RE_INHERIT = regex_possible_values 'inherit' RE_URI = Regexp.new('(url\([\s]*([\s]*' + RE_STRING.to_s + '[\s]*)[\s]*\))|(url\([\s]*([!#$%&*\-~]|' + RE_NON_ASCII.to_s + '|' + RE_ESCAPE.to_s + ')*[\s]*)\)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n') URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im RE_GRADIENT = /[-a-z]*gradient\([-a-z0-9 .,#%()]*\)/im # Initial parsing RE_AT_IMPORT_RULE = /\@import[\s]+(url\()?["''"]?(.[^'"\s"']*)["''"]?\)?([\w\s\,^\])]*)\)?;?/ #-- #RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")') #RE_AT_IMPORT_RULE = Regexp.new('@import[\s]*(' + RE_STRING.to_s + ')([\w\s\,]*)[;]?', Regexp::IGNORECASE) -- should handle url() even though it is not allowed #++ IMPORTANT_IN_PROPERTY_RX = /[\s]*!important\b[\s]*/i RE_INSIDE_OUTSIDE = regex_possible_values 'inside', 'outside' RE_SCROLL_FIXED = regex_possible_values 'scroll', 'fixed' RE_REPEAT = regex_possible_values 'repeat(\-x|\-y)*|no\-repeat' RE_LIST_STYLE_TYPE = regex_possible_values 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none' STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m # Special units BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE) RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED) FONT_UNITS_RX = /(([x]+\-)*small|medium|large[r]*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)*)/i RE_BORDER_STYLE = /([\s]*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)([\s]*$)?/imx RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i) # Patterns for specificity calculations NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX= / (\.[\w]+) # classes | \[(\w+) # attributes | (\:( # pseudo classes link|visited|active |hover|focus |lang |target |enabled|disabled|checked|indeterminate |root |nth-child|nth-last-child|nth-of-type|nth-last-of-type |first-child|last-child|first-of-type|last-of-type |only-child|only-of-type |empty|contains )) /ix ELEMENTS_AND_PSEUDO_ELEMENTS_RX = / ((^|[\s\+\>\~]+)[\w]+ # elements | \:{1,2}( # pseudo-elements after|before |first-letter|first-line |selection ) )/ix # Colours RE_COLOUR_NUMERIC = Regexp.new('((hsl|rgb)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE) RE_COLOUR_NUMERIC_ALPHA = Regexp.new('((hsla|rgba)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE) RE_COLOUR_HEX = /(#([0-9a-f]{6}|[0-9a-f]{3})([\s;]|$))/i RE_COLOUR_NAMED = /([\s]*^)?(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|transparent)([\s]*$)?/i RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED) # :startdoc: end css-parser-1.3.6/lib/css_parser/rule_set.rb0000644000175000017500000004271612542615072021114 0ustar terceiroterceiromodule CssParser class RuleSet # Patterns for specificity calculations RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment'] LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image'] # Array of selector strings. attr_reader :selectors # Integer with the specificity to use for this RuleSet. attr_accessor :specificity def initialize(selectors, block, specificity = nil) @selectors = [] @specificity = specificity @declarations = {} @order = 0 parse_selectors!(selectors) if selectors parse_declarations!(block) end # Get the value of a property def get_value(property) return '' unless property and not property.empty? property = property.downcase.strip properties = @declarations.inject('') do |val, (key, data)| #puts "COMPARING #{key} #{key.inspect} against #{property} #{property.inspect}" importance = data[:is_important] ? ' !important' : '' val << "#{data[:value]}#{importance}; " if key.downcase.strip == property val end return properties ? properties.strip : '' end alias_method :[], :get_value # Add a CSS declaration to the current RuleSet. # # rule_set.add_declaration!('color', 'blue') # # puts rule_set['color'] # => 'blue;' # # rule_set.add_declaration!('margin', '0px auto !important') # # puts rule_set['margin'] # => '0px auto !important;' # # If the property already exists its value will be over-written. def add_declaration!(property, value) if value.nil? or value.empty? @declarations.delete(property) return end value.gsub!(/;\Z/, '') is_important = !value.gsub!(CssParser::IMPORTANT_IN_PROPERTY_RX, '').nil? property = property.downcase.strip #puts "SAVING #{property} #{value} #{is_important.inspect}" @declarations[property] = { :value => value, :is_important => is_important, :order => @order += 1 } end alias_method :[]=, :add_declaration! # Remove CSS declaration from the current RuleSet. # # rule_set.remove_declaration!('color') def remove_declaration!(property) @declarations.delete(property) end # Iterate through selectors. # # Options # - +force_important+ -- boolean # # ==== Example # ruleset.each_selector do |sel, dec, spec| # ... # end def each_selector(options = {}) # :yields: selector, declarations, specificity declarations = declarations_to_s(options) if @specificity @selectors.each { |sel| yield sel.strip, declarations, @specificity } else @selectors.each { |sel| yield sel.strip, declarations, CssParser.calculate_specificity(sel) } end end # Iterate through declarations. def each_declaration # :yields: property, value, is_important decs = @declarations.sort { |a,b| a[1][:order].nil? || b[1][:order].nil? ? 0 : a[1][:order] <=> b[1][:order] } decs.each do |property, data| value = data[:value] yield property.downcase.strip, value.strip, data[:is_important] end end # Return all declarations as a string. #-- # TODO: Clean-up regexp doesn't seem to work #++ def declarations_to_s(options = {}) options = {:force_important => false}.merge(options) str = '' each_declaration do |prop, val, is_important| importance = (options[:force_important] || is_important) ? ' !important' : '' str += "#{prop}: #{val}#{importance}; " end str.gsub(/^[\s^(\{)]+|[\n\r\f\t]*|[\s]+$/mx, '').strip end # Return the CSS rule set as a string. def to_s decs = declarations_to_s "#{@selectors.join(',')} { #{decs} }" end # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts. def expand_shorthand! # border must be expanded before dimensions expand_border_shorthand! expand_dimensions_shorthand! expand_font_shorthand! expand_background_shorthand! expand_list_style_shorthand! end # Convert shorthand background declarations (e.g. background: url("chess.png") gray 50% repeat fixed;) # into their constituent parts. # # See http://www.w3.org/TR/CSS21/colors.html#propdef-background def expand_background_shorthand! # :nodoc: return unless @declarations.has_key?('background') value = @declarations['background'][:value] if value =~ CssParser::RE_INHERIT BACKGROUND_PROPERTIES.each do |prop| split_declaration('background', prop, 'inherit') end end split_declaration('background', 'background-image', value.slice!(Regexp.union(CssParser::URI_RX, CssParser::RE_GRADIENT, /none/i))) split_declaration('background', 'background-attachment', value.slice!(CssParser::RE_SCROLL_FIXED)) split_declaration('background', 'background-repeat', value.slice!(CssParser::RE_REPEAT)) split_declaration('background', 'background-color', value.slice!(CssParser::RE_COLOUR)) split_declaration('background', 'background-position', value.slice(CssParser::RE_BACKGROUND_POSITION)) @declarations.delete('background') end # Split shorthand border declarations (e.g. border: 1px red;) # Additional splitting happens in expand_dimensions_shorthand! def expand_border_shorthand! # :nodoc: ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].each do |k| next unless @declarations.has_key?(k) value = @declarations[k][:value] split_declaration(k, "#{k}-width", value.slice!(CssParser::RE_BORDER_UNITS)) split_declaration(k, "#{k}-color", value.slice!(CssParser::RE_COLOUR)) split_declaration(k, "#{k}-style", value.slice!(CssParser::RE_BORDER_STYLE)) @declarations.delete(k) end end # Split shorthand dimensional declarations (e.g. margin: 0px auto;) # into their constituent parts. Handles margin, padding, border-color, border-style and border-width. def expand_dimensions_shorthand! # :nodoc: {'margin' => 'margin-%s', 'padding' => 'padding-%s', 'border-color' => 'border-%s-color', 'border-style' => 'border-%s-style', 'border-width' => 'border-%s-width'}.each do |property, expanded| next unless @declarations.has_key?(property) value = @declarations[property][:value] # RGB and HSL values in borders are the only units that can have spaces (within params). # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we # can split easily on spaces. # # TODO: rgba, hsl, hsla value.gsub!(RE_COLOUR) { |c| c.gsub(/[\s]+/, '') } matches = value.strip.split(/[\s]+/) t, r, b, l = nil case matches.length when 1 t, r, b, l = matches[0], matches[0], matches[0], matches[0] when 2 t, b = matches[0], matches[0] r, l = matches[1], matches[1] when 3 t = matches[0] r, l = matches[1], matches[1] b = matches[2] when 4 t = matches[0] r = matches[1] b = matches[2] l = matches[3] end split_declaration(property, expanded % 'top', t) split_declaration(property, expanded % 'right', r) split_declaration(property, expanded % 'bottom', b) split_declaration(property, expanded % 'left', l) @declarations.delete(property) end end # Convert shorthand font declarations (e.g. font: 300 italic 11px/14px verdana, helvetica, sans-serif;) # into their constituent parts. def expand_font_shorthand! # :nodoc: return unless @declarations.has_key?('font') font_props = {} # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height'].each do |prop| font_props[prop] = 'normal' end value = @declarations['font'][:value] is_important = @declarations['font'][:is_important] order = @declarations['font'][:order] in_fonts = false matches = value.scan(/("(.*[^"])"|'(.*[^'])'|(\w[^ ,]+))/) matches.each do |match| m = match[0].to_s.strip m.gsub!(/[;]$/, '') if in_fonts if font_props.has_key?('font-family') font_props['font-family'] += ', ' + m else font_props['font-family'] = m end elsif m =~ /normal|inherit/i ['font-style', 'font-weight', 'font-variant'].each do |font_prop| font_props[font_prop] = m unless font_props.has_key?(font_prop) end elsif m =~ /italic|oblique/i font_props['font-style'] = m elsif m =~ /small\-caps/i font_props['font-variant'] = m elsif m =~ /[1-9]00$|bold|bolder|lighter/i font_props['font-weight'] = m elsif m =~ CssParser::FONT_UNITS_RX if m =~ /\// font_props['font-size'], font_props['line-height'] = m.split('/') else font_props['font-size'] = m end in_fonts = true end end font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important, :order => order} } @declarations.delete('font') end # Convert shorthand list-style declarations (e.g. list-style: lower-alpha outside;) # into their constituent parts. # # See http://www.w3.org/TR/CSS21/generate.html#lists def expand_list_style_shorthand! # :nodoc: return unless @declarations.has_key?('list-style') value = @declarations['list-style'][:value] if value =~ CssParser::RE_INHERIT LIST_STYLE_PROPERTIES.each do |prop| split_declaration('list-style', prop, 'inherit') end end split_declaration('list-style', 'list-style-type', value.slice!(CssParser::RE_LIST_STYLE_TYPE)) split_declaration('list-style', 'list-style-position', value.slice!(CssParser::RE_INSIDE_OUTSIDE)) split_declaration('list-style', 'list-style-image', value.slice!(Regexp.union(CssParser::URI_RX, /none/i))) @declarations.delete('list-style') end # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible. def create_shorthand! create_background_shorthand! create_dimensions_shorthand! # border must be shortened after dimensions create_border_shorthand! create_font_shorthand! create_list_style_shorthand! end # Combine several properties into a shorthand one def create_shorthand_properties! properties, shorthand_property # :nodoc: values = [] properties.each do |property| if @declarations.has_key?(property) and not @declarations[property][:is_important] values << @declarations[property][:value] @declarations.delete(property) end end unless values.empty? @declarations[shorthand_property] = {:value => values.join(' ')} end end # Looks for long format CSS background properties (e.g. background-color) and # converts them into a shorthand CSS background property. # # Leaves properties declared !important alone. def create_background_shorthand! # :nodoc: create_shorthand_properties! BACKGROUND_PROPERTIES, 'background' end # Combine border-color, border-style and border-width into border # Should be run after create_dimensions_shorthand! # # TODO: this is extremely similar to create_background_shorthand! and should be combined def create_border_shorthand! # :nodoc: values = [] ['border-width', 'border-style', 'border-color'].each do |property| if @declarations.has_key?(property) and not @declarations[property][:is_important] # can't merge if any value contains a space (i.e. has multiple values) # we temporarily remove any spaces after commas for the check (inside rgba, etc...) return if @declarations[property][:value].gsub(/\,[\s]/, ',').strip =~ /[\s]/ values << @declarations[property][:value] end end @declarations.delete('border-width') @declarations.delete('border-style') @declarations.delete('border-color') unless values.empty? @declarations['border'] = {:value => values.join(' ')} end end # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width) # and converts them into shorthand CSS properties. def create_dimensions_shorthand! # :nodoc: directions = ['top', 'right', 'bottom', 'left'] {'margin' => 'margin-%s', 'padding' => 'padding-%s', 'border-color' => 'border-%s-color', 'border-style' => 'border-%s-style', 'border-width' => 'border-%s-width'}.each do |property, expanded| top, right, bottom, left = ['top', 'right', 'bottom', 'left'].map { |side| expanded % side } foldable = @declarations.select do |dim, val| dim == top or dim == right or dim == bottom or dim == left end # All four dimensions must be present if foldable.length == 4 values = {} directions.each { |d| values[d.to_sym] = @declarations[expanded % d][:value].downcase.strip } if values[:left] == values[:right] if values[:top] == values[:bottom] if values[:top] == values[:left] # All four sides are equal new_value = values[:top] else # Top and bottom are equal, left and right are equal new_value = values[:top] + ' ' + values[:left] end else # Only left and right are equal new_value = values[:top] + ' ' + values[:left] + ' ' + values[:bottom] end else # No sides are equal new_value = values[:top] + ' ' + values[:right] + ' ' + values[:bottom] + ' ' + values[:left] end new_value.strip! @declarations[property] = {:value => new_value.strip} unless new_value.empty? # Delete the longhand values directions.each { |d| @declarations.delete(expanded % d) } end end end # Looks for long format CSS font properties (e.g. font-weight) and # tries to convert them into a shorthand CSS font property. All # font properties must be present in order to create a shorthand declaration. def create_font_shorthand! # :nodoc: ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].each do |prop| return unless @declarations.has_key?(prop) end new_value = '' ['font-style', 'font-variant', 'font-weight'].each do |property| unless @declarations[property][:value] == 'normal' new_value += @declarations[property][:value] + ' ' end end new_value += @declarations['font-size'][:value] unless @declarations['line-height'][:value] == 'normal' new_value += '/' + @declarations['line-height'][:value] end new_value += ' ' + @declarations['font-family'][:value] @declarations['font'] = {:value => new_value.gsub(/[\s]+/, ' ').strip} ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].each do |prop| @declarations.delete(prop) end end # Looks for long format CSS list-style properties (e.g. list-style-type) and # converts them into a shorthand CSS list-style property. # # Leaves properties declared !important alone. def create_list_style_shorthand! # :nodoc: create_shorthand_properties! LIST_STYLE_PROPERTIES, 'list-style' end private # utility method for re-assign shorthand elements to longhand versions def split_declaration(src, dest, v) # :nodoc: return unless v and not v.empty? if @declarations.has_key?(dest) #puts "dest #{dest} already exists" if @declarations[dest][:order] > @declarations[src][:order] #puts "skipping #{dest}:#{v} due to order " return else @declarations[dest] = {} end end @declarations[dest] = @declarations[src].merge({:value => v.to_s.strip}) end def parse_declarations!(block) # :nodoc: @declarations = {} return unless block block.gsub!(/(^[\s]*)|([\s]*$)/, '') continuation = '' block.split(/[\;$]+/m).each do |decs| decs = continuation + decs if decs =~ /\([^)]*\Z/ # if it has an unmatched parenthesis continuation = decs + ';' elsif matches = decs.match(/(.[^:]*)\s*:\s*(.+)(;?\s*\Z)/i) property, value, end_of_declaration = matches.captures add_declaration!(property, value) continuation = '' end end end #-- # TODO: way too simplistic #++ def parse_selectors!(selectors) # :nodoc: @selectors = selectors.split(',').map { |s| s.strip } end end end css-parser-1.3.6/lib/css_parser/parser.rb0000644000175000017500000004005612542615072020561 0ustar terceiroterceiromodule CssParser # Exception class used for any errors encountered while downloading remote files. class RemoteFileError < IOError; end # Exception class used if a request is made to load a CSS file more than once. class CircularReferenceError < StandardError; end # == Parser class # # All CSS is converted to UTF-8. # # When calling Parser#new there are some configuaration options: # [absolute_paths] Convert relative paths to absolute paths (href, src and url(''). Boolean, default is false. # [import] Follow @import rules. Boolean, default is true. # [io_exceptions] Throw an exception if a link can not be found. Boolean, default is true. class Parser USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (http://github.com/alexdunae/css_parser)" STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m # Initial parsing RE_AT_IMPORT_RULE = /\@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s\)]*)["']?\)?([\w\s\,^\]\(\))]*)\)?[;\n]?/ # Array of CSS files that have been loaded. attr_reader :loaded_uris #-- # Class variable? see http://www.oreillynet.com/ruby/blog/2007/01/nubygems_dont_use_class_variab_1.html #++ @folded_declaration_cache = {} class << self; attr_reader :folded_declaration_cache; end def initialize(options = {}) @options = {:absolute_paths => false, :import => true, :io_exceptions => true}.merge(options) # array of RuleSets @rules = [] @loaded_uris = [] # unprocessed blocks of CSS @blocks = [] reset! end # Get declarations by selector. # # +media_types+ are optional, and can be a symbol or an array of symbols. # The default value is :all. # # ==== Examples # find_by_selector('#content') # => 'font-size: 13px; line-height: 1.2;' # # find_by_selector('#content', [:screen, :handheld]) # => 'font-size: 13px; line-height: 1.2;' # # find_by_selector('#content', :print) # => 'font-size: 11pt; line-height: 1.2;' # # Returns an array of declarations. def find_by_selector(selector, media_types = :all) out = [] each_selector(media_types) do |sel, dec, spec| out << dec if sel.strip == selector.strip end out end alias_method :[], :find_by_selector # Finds the rule sets that match the given selectors def find_rule_sets(selectors, media_types = :all) rule_sets = [] selectors.each do |selector| each_rule_set(media_types) do |rule_set, media_type| if !rule_sets.member?(rule_set) && rule_set.selectors.member?(selector) rule_sets << rule_set end end end rule_sets end # Add a raw block of CSS. # # In order to follow +@import+ rules you must supply either a # +:base_dir+ or +:base_uri+ option. # # Use the +:media_types+ option to set the media type(s) for this block. Takes an array of symbols. # # Use the +:only_media_types+ option to selectively follow +@import+ rules. Takes an array of symbols. # # ==== Example # css = <<-EOT # body { font-size: 10pt } # p { margin: 0px; } # @media screen, print { # body { line-height: 1.2 } # } # EOT # # parser = CssParser::Parser.new # parser.add_block!(css) def add_block!(block, options = {}) options = {:base_uri => nil, :base_dir => nil, :charset => nil, :media_types => :all, :only_media_types => :all}.merge(options) options[:media_types] = [options[:media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)} options[:only_media_types] = [options[:only_media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)} block = cleanup_block(block) if options[:base_uri] and @options[:absolute_paths] block = CssParser.convert_uris(block, options[:base_uri]) end # Load @imported CSS if @options[:import] block.scan(RE_AT_IMPORT_RULE).each do |import_rule| media_types = [] if media_string = import_rule[-1] media_string.split(/[,]/).each do |t| media_types << CssParser.sanitize_media_query(t) unless t.empty? end else media_types = [:all] end next unless options[:only_media_types].include?(:all) or media_types.length < 1 or (media_types & options[:only_media_types]).length > 0 import_path = import_rule[0].to_s.gsub(/['"]*/, '').strip if options[:base_uri] import_uri = Addressable::URI.parse(options[:base_uri].to_s) + Addressable::URI.parse(import_path) load_uri!(import_uri, options[:base_uri], media_types) elsif options[:base_dir] load_file!(import_path, options[:base_dir], media_types) end end end # Remove @import declarations block.gsub!(RE_AT_IMPORT_RULE, '') parse_block_into_rule_sets!(block, options) end # Add a CSS rule by setting the +selectors+, +declarations+ and +media_types+. # # +media_types+ can be a symbol or an array of symbols. def add_rule!(selectors, declarations, media_types = :all) rule_set = RuleSet.new(selectors, declarations) add_rule_set!(rule_set, media_types) end # Add a CssParser RuleSet object. # # +media_types+ can be a symbol or an array of symbols. def add_rule_set!(ruleset, media_types = :all) raise ArgumentError unless ruleset.kind_of?(CssParser::RuleSet) media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)} @rules << {:media_types => media_types, :rules => ruleset} end # Iterate through RuleSet objects. # # +media_types+ can be a symbol or an array of symbols. def each_rule_set(media_types = :all) # :yields: rule_set, media_types media_types = [:all] if media_types.nil? media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)} @rules.each do |block| if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) } yield(block[:rules], block[:media_types]) end end end # Iterate through CSS selectors. # # +media_types+ can be a symbol or an array of symbols. # See RuleSet#each_selector for +options+. def each_selector(media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types each_rule_set(media_types) do |rule_set, media_types| rule_set.each_selector(options) do |selectors, declarations, specificity| yield selectors, declarations, specificity, media_types end end end # Output all CSS rules as a single stylesheet. def to_s(media_types = :all) out = '' styles_by_media_types = {} each_selector(media_types) do |selectors, declarations, specificity, media_types| media_types.each do |media_type| styles_by_media_types[media_type] ||= [] styles_by_media_types[media_type] << [selectors, declarations] end end styles_by_media_types.each_pair do |media_type, media_styles| media_block = (media_type != :all) out += "@media #{media_type} {\n" if media_block media_styles.each do |media_style| if media_block out += " #{media_style[0]} {\n #{media_style[1]}\n }\n" else out += "#{media_style[0]} {\n#{media_style[1]}\n}\n" end end out += "}\n" if media_block end out end # A hash of { :media_query => rule_sets } def rules_by_media_query rules_by_media = {} @rules.each do |block| block[:media_types].each do |mt| unless rules_by_media.has_key?(mt) rules_by_media[mt] = [] end rules_by_media[mt] << block[:rules] end end rules_by_media end # Merge declarations with the same selector. def compact! # :nodoc: compacted = [] compacted end def parse_block_into_rule_sets!(block, options = {}) # :nodoc: current_media_queries = [:all] if options[:media_types] current_media_queries = options[:media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)} end in_declarations = 0 block_depth = 0 in_charset = false # @charset is ignored for now in_string = false in_at_media_rule = false in_media_block = false current_selectors = '' current_media_query = '' current_declarations = '' block.scan(/([\\]?[{}\s"]|(.[^\s"{}\\]*))/).each do |matches| token = matches[0] if token =~ /\A"/ # found un-escaped double quote in_string = !in_string end if in_declarations > 0 # too deep, malformed declaration block if in_declarations > 1 in_declarations -= 1 if token =~ /\}/ next end if token =~ /\{/ in_declarations += 1 next end current_declarations += token if token =~ /\}/ and not in_string current_declarations.gsub!(/\}[\s]*$/, '') in_declarations -= 1 unless current_declarations.strip.empty? add_rule!(current_selectors, current_declarations, current_media_queries) end current_selectors = '' current_declarations = '' end elsif token =~ /@media/i # found '@media', reset current media_types in_at_media_rule = true current_media_queries = [] elsif in_at_media_rule if token =~ /\{/ block_depth = block_depth + 1 in_at_media_rule = false in_media_block = true current_media_queries << CssParser.sanitize_media_query(current_media_query) current_media_query = '' elsif token =~ /[,]/ # new media query begins token.gsub!(/[,]/, ' ') current_media_query += token.strip + ' ' current_media_queries << CssParser.sanitize_media_query(current_media_query) current_media_query = '' else current_media_query += token.strip + ' ' end elsif in_charset or token =~ /@charset/i # iterate until we are out of the charset declaration in_charset = (token =~ /;/ ? false : true) else if token =~ /\}/ and not in_string block_depth = block_depth - 1 # reset the current media query scope if in_media_block current_media_queries = [:all] in_media_block = false end else if token =~ /\{/ and not in_string current_selectors.gsub!(/^[\s]*/, '') current_selectors.gsub!(/[\s]*$/, '') in_declarations += 1 else current_selectors += token end end end end # check for unclosed braces if in_declarations > 0 add_rule!(current_selectors, current_declarations, current_media_queries) end end # Load a remote CSS file. # # You can also pass in file://test.css # # See add_block! for options. # # Deprecated: originally accepted three params: `uri`, `base_uri` and `media_types` def load_uri!(uri, options = {}, deprecated = nil) uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme #base_uri = nil, media_types = :all, options = {} opts = {:base_uri => nil, :media_types => :all} if options.is_a? Hash opts.merge!(options) else opts[:base_uri] = options if options.is_a? String opts[:media_types] = deprecated if deprecated end if uri.scheme == 'file' or uri.scheme.nil? uri.path = File.expand_path(uri.path) uri.scheme = 'file' end opts[:base_uri] = uri if opts[:base_uri].nil? src, charset = read_remote_file(uri) if src add_block!(src, opts) end end # Load a local CSS file. def load_file!(file_name, base_dir = nil, media_types = :all) file_name = File.expand_path(file_name, base_dir) return unless File.readable?(file_name) return unless circular_reference_check(file_name) src = IO.read(file_name) base_dir = File.dirname(file_name) add_block!(src, {:media_types => media_types, :base_dir => base_dir}) end # Load a local CSS string. def load_string!(src, base_dir = nil, media_types = :all) add_block!(src, {:media_types => media_types, :base_dir => base_dir}) end protected # Check that a path hasn't been loaded already # # Raises a CircularReferenceError exception if io_exceptions are on, # otherwise returns true/false. def circular_reference_check(path) path = path.to_s if @loaded_uris.include?(path) raise CircularReferenceError, "can't load #{path} more than once" if @options[:io_exceptions] return false else @loaded_uris << path return true end end # Strip comments and clean up blank lines from a block of CSS. # # Returns a string. def cleanup_block(block) # :nodoc: # Strip CSS comments block.gsub!(STRIP_CSS_COMMENTS_RX, '') # Strip HTML comments - they shouldn't really be in here but # some people are just crazy... block.gsub!(STRIP_HTML_COMMENTS_RX, '') # Strip lines containing just whitespace block.gsub!(/^\s+$/, "") block end # Download a file into a string. # # Returns the file's data and character set in an array. #-- # TODO: add option to fail silently or throw and exception on a 404 #++ def read_remote_file(uri) # :nodoc: return nil, nil unless circular_reference_check(uri.to_s) src = '', charset = nil begin uri = Addressable::URI.parse(uri.to_s) if uri.scheme == 'file' # local file fh = open(uri.path, 'rb') src = fh.read fh.close else # remote file if uri.scheme == 'https' uri.port = 443 unless uri.port http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE else http = Net::HTTP.new(uri.host, uri.port) end res = http.get(uri.request_uri, {'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip'}) src = res.body charset = fh.respond_to?(:charset) ? fh.charset : 'utf-8' if res.code.to_i >= 400 raise RemoteFileError if @options[:io_exceptions] return '', nil end case res['content-encoding'] when 'gzip' io = Zlib::GzipReader.new(StringIO.new(res.body)) src = io.read when 'deflate' io = Zlib::Inflate.new src = io.inflate(res.body) end end if charset if String.method_defined?(:encode) src.encode!('UTF-8', charset) else ic = Iconv.new('UTF-8//IGNORE', charset) src = ic.iconv(src) end end rescue raise RemoteFileError if @options[:io_exceptions] return nil, nil end return src, charset end private # Save a folded declaration block to the internal cache. def save_folded_declaration(block_hash, folded_declaration) # :nodoc: @folded_declaration_cache[block_hash] = folded_declaration end # Retrieve a folded declaration block from the internal cache. def get_folded_declaration(block_hash) # :nodoc: return @folded_declaration_cache[block_hash] ||= nil end def reset! # :nodoc: @folded_declaration_cache = {} @css_source = '' @css_rules = [] @css_warnings = [] end end end css-parser-1.3.6/lib/css_parser/version.rb0000644000175000017500000000006012542615072020741 0ustar terceiroterceiromodule CssParser VERSION = "1.3.6".freeze end css-parser-1.3.6/lib/css_parser.rb0000644000175000017500000001230712542615072017263 0ustar terceiroterceirorequire 'addressable/uri' require 'uri' require 'net/https' require 'digest/md5' require 'zlib' require 'stringio' require 'iconv' unless String.method_defined?(:encode) 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].kind_of?(Array) unless rule_sets.all? {|rs| rs.kind_of?(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 unless specificity if rule_set.selectors.length == 0 specificity = 0 else specificity = rule_set.selectors.map { |s| calculate_specificity(s) }.compact.max || 0 end end 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 not properties.has_key?(property) properties[property] = {:value => value, :specificity => specificity, :is_important => is_important} elsif is_important if not properties[property][:is_important] or properties[property][:specificity] <= specificity properties[property] = {:value => value, :specificity => specificity, :is_important => is_important} end elsif properties[property][:specificity] < specificity or properties[property][:specificity] == specificity unless properties[property][:is_important] properties[property] = {:value => value, :specificity => specificity, :is_important => is_important} end end end end merged = RuleSet.new(nil, nil) properties.each do |property, details| if details[:is_important] merged[property.strip] = details[:value].strip.gsub(/\;\Z/, '') + '!important' else merged[property.strip] = details[:value].strip end 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).length d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX).length (a.to_s + b.to_s + c.to_s + d.to_s).to_i rescue return 0 end # Make url() 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.kind_of?(Addressable::URI) css.gsub(URI_RX) do uri = $1.to_s uri.gsub!(/["']+/, '') # Don't process URLs that are already absolute unless uri =~ /^[a-z]+\:\/\//i begin uri = base_uri + uri rescue; end end "url('#{uri.to_s}')" end end def self.sanitize_media_query(raw) mq = raw.to_s.gsub(/[\s]+/, ' ').strip mq = 'all' if mq.empty? mq.to_sym end end css-parser-1.3.6/Gemfile.lock0000644000175000017500000000062712542615072016250 0ustar terceiroterceiroPATH remote: . specs: css_parser (1.3.6) addressable GEM remote: https://rubygems.org/ specs: addressable (2.3.6) bouncy-castle-java (1.5.0146.1) bump (0.3.12) jruby-openssl (0.7.6.1) bouncy-castle-java (>= 1.5.0146.1) rake (0.9.2.2) test-unit (2.5.4) PLATFORMS java ruby DEPENDENCIES bump css_parser! jruby-openssl rake test-unit (>= 2.5.3) css-parser-1.3.6/MIT-LICENSE0000644000175000017500000000207512542615072015461 0ustar terceiroterceiro=== Ruby CSS Parser License Copyright (c) 2007-11 Alex Dunae Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.css-parser-1.3.6/Readme.md0000644000175000017500000000314012542615072015536 0ustar terceiroterceiro# Ruby CSS Parser Load, parse and cascade CSS rule sets in Ruby. # Setup ```Bash gem install css_parser ``` # Usage ```Ruby require 'css_parser' include CssParser parser = CssParser::Parser.new parser.load_uri!('http://example.com/styles/style.css') parser = CssParser::Parser.new parser.load_uri!('file://home/user/styles/style.css') # load a remote file, setting the base_uri and media_types parser.load_uri!('../style.css', {:base_uri => 'http://example.com/styles/inc/', :media_types => [:screen, :handheld]) # load a local file, setting the base_dir and media_types parser.load_file!('print.css', '~/styles/', :print) # lookup a rule by a selector parser.find_by_selector('#content') #=> 'font-size: 13px; line-height: 1.2;' # lookup a rule by a selector and media type parser.find_by_selector('#content', [:screen, :handheld]) # iterate through selectors by media type parser.each_selector(:screen) do |selector, declarations, specificity| ... end # add a block of CSS css = <<-EOT body { margin: 0 1em; } EOT parser.add_block!(css) # output all CSS rules in a single stylesheet parser.to_s => #content { font-size: 13px; line-height: 1.2; } body { margin: 0 1em; } ``` # Testing ```Bash bundle bundle exec rake ``` Runs on Ruby/JRuby 1.9.2 or above. # Credits By Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-11. License: MIT Thanks to [all the wonderful contributors](http://github.com/alexdunae/css_parser/contributors) for their updates. Made on Vancouver Island. [![Build Status](https://travis-ci.org/alexdunae/css_parser.png)](https://travis-ci.org/alexdunae/css_parser) css-parser-1.3.6/test/0000755000175000017500000000000012542615072015000 5ustar terceiroterceirocss-parser-1.3.6/test/test_css_parser_loading.rb0000644000175000017500000001163012542615072022226 0ustar terceiroterceirorequire File.expand_path(File.dirname(__FILE__) + '/test_helper') # Test cases for the CssParser's loading functions. class CssParserLoadingTests < Test::Unit::TestCase include CssParser include WEBrick def setup # from http://nullref.se/blog/2006/5/17/testing-with-webrick @cp = Parser.new @uri_base = 'http://localhost:12000' @www_root = File.dirname(__FILE__) + '/fixtures/' @server_thread = Thread.new do s = WEBrick::HTTPServer.new(:Port => 12000, :DocumentRoot => @www_root, :Logger => Log.new(nil, BasicLog::FATAL), :AccessLog => []) @port = s.config[:Port] begin s.start ensure s.shutdown end end sleep 1 # ensure the server has time to load end def teardown @server_thread.kill @server_thread.join(5) @server_thread = nil end def test_loading_a_local_file file_name = File.dirname(__FILE__) + '/fixtures/simple.css' @cp.load_file!(file_name) assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ') end def test_loading_a_local_file_with_scheme file_name = 'file://' + File.expand_path(File.dirname(__FILE__)) + '/fixtures/simple.css' @cp.load_uri!(file_name) assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ') end def test_loading_a_remote_file @cp.load_uri!("#{@uri_base}/simple.css") assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ') end # http://github.com/alexdunae/css_parser/issues#issue/4 def test_loading_a_remote_file_over_ssl # TODO: test SSL locally @cp.load_uri!("https://dialect.ca/inc/screen.css") assert_match /margin\: 0\;/, @cp.find_by_selector('body').join(' ') end def test_loading_a_string @cp.load_string!("p{margin:0px}") assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ') end def test_following_at_import_rules_local base_dir = File.dirname(__FILE__) + '/fixtures' @cp.load_file!('import1.css', base_dir) # from '/import1.css' assert_equal 'color: lime;', @cp.find_by_selector('div').join(' ') # from '/subdir/import2.css' assert_equal 'text-decoration: none;', @cp.find_by_selector('a').join(' ') # from '/subdir/../simple.css' assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ') end def test_following_at_import_rules_remote @cp.load_uri!("#{@uri_base}/import1.css") # from '/import1.css' assert_equal 'color: lime;', @cp.find_by_selector('div').join(' ') # from '/subdir/import2.css' assert_equal 'text-decoration: none;', @cp.find_by_selector('a').join(' ') # from '/subdir/../simple.css' assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ') end def test_imports_disabled cp = Parser.new(:import => false) cp.load_uri!("#{@uri_base}/import1.css") # from '/import1.css' assert_equal 'color: lime;', cp.find_by_selector('div').join(' ') # from '/subdir/import2.css' assert_equal '', cp.find_by_selector('a').join(' ') # from '/subdir/../simple.css' assert_equal '', cp.find_by_selector('p').join(' ') end def test_following_badly_escaped_import_rules css_block = '@import "http://example.com/css?family=Droid+Sans:regular,bold|Droid+Serif:regular,italic,bold,bolditalic&subset=latin";' assert_nothing_raised do @cp.add_block!(css_block, :base_uri => "#{@uri_base}/subdir/") end end def test_following_at_import_rules_from_add_block css_block = '@import "../simple.css";' @cp.add_block!(css_block, :base_uri => "#{@uri_base}/subdir/") # from 'simple.css' assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ') end def test_importing_with_media_types @cp.load_uri!("#{@uri_base}/import-with-media-types.css") # from simple.css with :screen media type assert_equal 'margin: 0px;', @cp.find_by_selector('p', :screen).join(' ') assert_equal '', @cp.find_by_selector('p', :tty).join(' ') end def test_local_circular_reference_exception assert_raise CircularReferenceError do @cp.load_file!(File.dirname(__FILE__) + '/fixtures/import-circular-reference.css') end end def test_remote_circular_reference_exception assert_raise CircularReferenceError do @cp.load_uri!("#{@uri_base}/import-circular-reference.css") end end def test_suppressing_circular_reference_exceptions cp_without_exceptions = Parser.new(:io_exceptions => false) assert_nothing_raised CircularReferenceError do cp_without_exceptions.load_uri!("#{@uri_base}/import-circular-reference.css") end end def test_toggling_not_found_exceptions cp_with_exceptions = Parser.new(:io_exceptions => true) assert_raise RemoteFileError do cp_with_exceptions.load_uri!("#{@uri_base}/no-exist.xyz") end cp_without_exceptions = Parser.new(:io_exceptions => false) assert_nothing_raised RemoteFileError do cp_without_exceptions.load_uri!("#{@uri_base}/no-exist.xyz") end end end css-parser-1.3.6/test/test_helper.rb0000644000175000017500000000023212542615072017640 0ustar terceiroterceirorequire 'rubygems' require 'bundler/setup' require 'test/unit' require 'net/http' require 'webrick' require File.dirname(__FILE__) + '/../lib/css_parser' css-parser-1.3.6/test/test_css_parser_regexps.rb0000644000175000017500000000641112542615072022267 0ustar terceiroterceiro# coding: iso-8859-1 require File.expand_path(File.dirname(__FILE__) + '/test_helper') # Test cases for CSS regular expressions # # see http://www.w3.org/TR/CSS21/syndata.html and # http://www.w3.org/TR/CSS21/grammar.html class CssParserRegexpTests < Test::Unit::TestCase def test_strings # complete matches [ '"abcd"', '" A sd sédrcv \'dsf\' asd rfg asd"', '"A\ d??ef 123!"', "\"this is\\\n a test\"", '"back\67round"', '"r\000065 ed"', "'abcd'", "' A sd sedrcv \"dsf\" asd rf—&23$%#%$g asd'", "'A\\\n def 123!'", "'this is\\\n a test'", "'back\\67round'", "'r\\000065 ed'" ].each do |str| assert_equal str, str.match(CssParser::RE_STRING).to_s end test_string = "p { background: red url(\"url\\.'p'ng\"); }" assert_equal "\"url\\.'p'ng\"", test_string.match(CssParser::RE_STRING).to_s end def test_unicode ['back\67round', 'r\000065 ed', '\00006C'].each do |str| assert_match(Regexp.new(CssParser::RE_UNICODE), str) end end def test_colour [ 'color: #fff', 'color:#f0a09c;', 'color: #04A', 'color: #04a9CE', 'color: rgb(100, -10%, 300);', 'color: rgb(10,10,10)', 'color:rgb(12.7253%, -12%,0)', 'color: black', 'color:Red;', 'color: AqUa;', 'color: blue ', 'color: transparent' ].each do |colour| assert_match(CssParser::RE_COLOUR, colour) end [ 'color: #fa', 'color:#f009c;', 'color: #04G', 'color: #04a9Cq', 'color: rgb 100, -10%, 300;', 'color: rgb 10,10,10', 'color:rgb(12px, -12%,0)', 'color:fuscia;', 'color: thick' ].each do |colour| assert_no_match(CssParser::RE_COLOUR, colour) end end def test_gradients [ 'linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)', 'linear-gradient(top, hsla(0, 0%, 0%, 0.00) 0%, hsla(0, 0%, 0%, 0.20) 100%)', '-o-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)', '-moz-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)', '-webkit-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)', '-webkit-gradient(linear, left top, left bottom, color-stop(0, hsla(0, 0%, 0%, 0.00)), color-stop(1, hsla(0, 0%, 0%, 0.20)))', '-ms-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)' ].each do |grad| assert_match(CssParser::RE_GRADIENT, grad) end end def test_uris crazy_uri = 'http://www.example.com:80/~/redb%20all.png?test=test&test;test+test#test!' assert_equal "url('#{crazy_uri}')", "li { list-style: url('#{crazy_uri}') disc }".match(CssParser::RE_URI).to_s assert_equal "url(#{crazy_uri})", "li { list-style: url(#{crazy_uri}) disc }".match(CssParser::RE_URI).to_s assert_equal "url(\"#{crazy_uri}\")", "li { list-style: url(\"#{crazy_uri}\") disc }".match(CssParser::RE_URI).to_s end def test_important assert_match(CssParser::IMPORTANT_IN_PROPERTY_RX, "color: #f00 !important ;") assert_no_match(CssParser::IMPORTANT_IN_PROPERTY_RX, "color: #f00 !importantish;") end protected def load_test_file(filename) fh = File.new("fixtures/#{filename}", 'r') test_file = fh.read fh.close return test_file end end css-parser-1.3.6/test/fixtures/0000755000175000017500000000000012542615072016651 5ustar terceiroterceirocss-parser-1.3.6/test/fixtures/import-circular-reference.css0000644000175000017500000000014712542615072024435 0ustar terceiroterceiro@import "import-circular-reference.css"; body { color: black; background: white; } p { margin: 0px; } css-parser-1.3.6/test/fixtures/subdir/0000755000175000017500000000000012542615072020141 5ustar terceiroterceirocss-parser-1.3.6/test/fixtures/subdir/import2.css0000644000175000017500000000006712542615072022252 0ustar terceiroterceiro@import "../simple.css"; a { text-decoration: none; } css-parser-1.3.6/test/fixtures/import1.css0000644000175000017500000000006412542615072020756 0ustar terceiroterceiro@import 'subdir/import2.css'; div { color: lime; } css-parser-1.3.6/test/fixtures/simple.css0000644000175000017500000000010012542615072020643 0ustar terceiroterceirobody { color: black; background: white; } p { margin: 0px; } css-parser-1.3.6/test/fixtures/import-with-media-types.css0000644000175000017500000000007612542615072024070 0ustar terceiroterceiro@import "simple.css" print, tv, screen; div { color: lime; } css-parser-1.3.6/test/test_css_parser_media_types.rb0000644000175000017500000001017512542615072023117 0ustar terceiroterceirorequire File.expand_path(File.dirname(__FILE__) + '/test_helper') # Test cases for the handling of media types class CssParserMediaTypesTests < Test::Unit::TestCase include CssParser def setup @cp = Parser.new end def test_that_media_types_dont_include_all @cp.add_block!(<<-CSS) @media handheld { body { color: blue; } p { color: grey; } } @media screen { body { color: red; } } CSS rules = @cp.rules_by_media_query assert_equal [ "handheld", "screen" ], rules.keys.map { |k| k.to_s }.sort end def test_finding_by_media_type # from http://www.w3.org/TR/CSS21/media.html#at-media-rule @cp.add_block!(<<-CSS) @media print { body { font-size: 10pt } } @media screen { body { font-size: 13px } } @media screen, print { body { line-height: 1.2 } } @media screen, 3d-glasses, print and resolution > 90dpi { body { color: blue; } } CSS assert_equal 'font-size: 10pt; line-height: 1.2;', @cp.find_by_selector('body', :print).join(' ') assert_equal 'font-size: 13px; line-height: 1.2; color: blue;', @cp.find_by_selector('body', :screen).join(' ') assert_equal 'color: blue;', @cp.find_by_selector('body', 'print and resolution > 90dpi'.to_sym).join(' ') end def test_finding_by_multiple_media_types @cp.add_block!(<<-CSS) @media print { body { font-size: 10pt } } @media handheld { body { font-size: 13px } } @media screen, print { body { line-height: 1.2 } } CSS assert_equal 'font-size: 13px; line-height: 1.2;', @cp.find_by_selector('body', [:screen,:handheld]).join(' ') end def test_adding_block_with_media_types @cp.add_block!(<<-CSS, :media_types => [:screen]) body { font-size: 10pt } CSS assert_equal 'font-size: 10pt;', @cp.find_by_selector('body', :screen).join(' ') assert @cp.find_by_selector('body', :handheld).empty? end def test_adding_block_with_media_types_followed_by_general_rule @cp.add_block!(<<-CSS) @media print { body { font-size: 10pt } } body { color: black; } CSS assert_match 'color: black;', @cp.to_s end def test_adding_block_and_limiting_media_types1 css = <<-CSS @import "import1.css", print CSS base_dir = File.dirname(__FILE__) + '/fixtures/' @cp.add_block!(css, :only_media_types => :screen, :base_dir => base_dir) assert @cp.find_by_selector('div').empty? end def test_adding_block_and_limiting_media_types2 css = <<-CSS @import "import1.css", print and (color) CSS base_dir = File.dirname(__FILE__) + '/fixtures/' @cp.add_block!(css, :only_media_types => 'print and (color)', :base_dir => base_dir) assert_match 'color: lime', @cp.find_by_selector('div').join(' ') end def test_adding_block_and_limiting_media_types css = <<-CSS @import "import1.css" CSS base_dir = File.dirname(__FILE__) + '/fixtures/' @cp.add_block!(css, :only_media_types => :print, :base_dir => base_dir) assert_match '', @cp.find_by_selector('div').join(' ') end def test_adding_rule_set_with_media_type @cp.add_rule!('body', 'color: black;', [:handheld,:tty]) @cp.add_rule!('body', 'color: blue;', :screen) assert_equal 'color: black;', @cp.find_by_selector('body', :handheld).join(' ') end def test_adding_rule_set_with_media_query @cp.add_rule!('body', 'color: black;', 'aural and (device-aspect-ratio: 16/9)') assert_equal 'color: black;', @cp.find_by_selector('body', 'aural and (device-aspect-ratio: 16/9)').join(' ') assert_equal 'color: black;', @cp.find_by_selector('body', :all).join(' ') end def test_selecting_with_all_media_types @cp.add_rule!('body', 'color: black;', [:handheld,:tty]) assert_equal 'color: black;', @cp.find_by_selector('body', :all).join(' ') end def test_to_s_includes_media_queries @cp.add_rule!('body', 'color: black;', 'aural and (device-aspect-ratio: 16/9)') assert_equal "@media aural and (device-aspect-ratio: 16/9) {\n body {\n color: black;\n }\n}\n", @cp.to_s end end css-parser-1.3.6/test/test_css_parser_basic.rb0000644000175000017500000000421012542615072021666 0ustar terceiroterceirorequire File.expand_path(File.dirname(__FILE__) + '/test_helper') # Test cases for reading and generating CSS shorthand properties class CssParserBasicTests < Test::Unit::TestCase include CssParser def setup @cp = CssParser::Parser.new @css = <<-CSS html, body, p { margin: 0px; } p { padding: 0px; } #content { font: 12px/normal sans-serif; } .content { color: red; } CSS end def test_finding_by_selector @cp.add_block!(@css) assert_equal 'margin: 0px;', @cp.find_by_selector('body').join(' ') assert_equal 'margin: 0px; padding: 0px;', @cp.find_by_selector('p').join(' ') assert_equal 'font: 12px/normal sans-serif;', @cp.find_by_selector('#content').join(' ') assert_equal 'color: red;', @cp.find_by_selector('.content').join(' ') end def test_adding_block @cp.add_block!(@css) assert_equal 'margin: 0px;', @cp.find_by_selector('body').join end def test_adding_block_without_closing_brace @cp.add_block!('p { color: red;') assert_equal 'color: red;', @cp.find_by_selector('p').join end def test_adding_a_rule @cp.add_rule!('div', 'color: blue;') assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ') end def test_adding_a_rule_set rs = CssParser::RuleSet.new('div', 'color: blue;') @cp.add_rule_set!(rs) assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ') end def test_toggling_uri_conversion # with conversion cp_with_conversion = Parser.new(:absolute_paths => true) cp_with_conversion.add_block!("body { background: url('../style/yellow.png?abc=123') };", :base_uri => 'http://example.org/style/basic.css') assert_equal "background: url('http://example.org/style/yellow.png?abc=123');", cp_with_conversion['body'].join(' ') # without conversion cp_without_conversion = Parser.new(:absolute_paths => false) cp_without_conversion.add_block!("body { background: url('../style/yellow.png?abc=123') };", :base_uri => 'http://example.org/style/basic.css') assert_equal "background: url('../style/yellow.png?abc=123');", cp_without_conversion['body'].join(' ') end end css-parser-1.3.6/test/test_rule_set_expanding_shorthand.rb0000644000175000017500000002323512542615072024322 0ustar terceiroterceirorequire File.expand_path(File.dirname(__FILE__) + '/test_helper') class RuleSetExpandingShorthandTests < Test::Unit::TestCase include CssParser def setup @cp = CssParser::Parser.new end # Dimensions shorthand def test_expanding_border_shorthand declarations = expand_declarations('border: none') assert_equal 'none', declarations['border-right-style'] declarations = expand_declarations('border: 1px solid red') assert_equal '1px', declarations['border-top-width'] assert_equal 'solid', declarations['border-bottom-style'] declarations = expand_declarations('border-color: red hsla(255, 0, 0, 5) rgb(2% ,2%,2%)') assert_equal 'red', declarations['border-top-color'] assert_equal 'rgb(2%,2%,2%)', declarations['border-bottom-color'] assert_equal 'hsla(255,0,0,5)', declarations['border-left-color'] declarations = expand_declarations('border: thin dot-dot-dash') assert_equal 'dot-dot-dash', declarations['border-left-style'] assert_equal 'thin', declarations['border-left-width'] assert_nil declarations['border-left-color'] end # Dimensions shorthand def test_getting_dimensions_from_shorthand # test various shorthand forms ['margin: 0px auto', 'margin: 0px auto 0px', 'margin: 0px auto 0px'].each do |shorthand| declarations = expand_declarations(shorthand) assert_equal({"margin-right" => "auto", "margin-bottom" => "0px", "margin-left" => "auto", "margin-top" => "0px"}, declarations) end # test various units ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit| shorthand = "margin: 0% -0.123#{unit} 9px -.9pc" declarations = expand_declarations(shorthand) assert_equal({"margin-right" => "-0.123#{unit}", "margin-bottom" => "9px", "margin-left" => "-.9pc", "margin-top" => "0%"}, declarations) end end # Font shorthand def test_getting_font_size_from_shorthand ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit| shorthand = "font: 300 italic 11.25#{unit}/14px verdana, helvetica, sans-serif;" declarations = expand_declarations(shorthand) assert_equal("11.25#{unit}", declarations['font-size']) end ['smaller', 'small', 'medium', 'large', 'x-large', 'auto'].each do |unit| shorthand = "font: 300 italic #{unit}/14px verdana, helvetica, sans-serif;" declarations = expand_declarations(shorthand) assert_equal(unit, declarations['font-size']) end end def test_getting_font_families_from_shorthand shorthand = "font: 300 italic 12px/14px \"Helvetica-Neue-Light 45\", 'verdana', helvetica, sans-serif;" declarations = expand_declarations(shorthand) assert_equal("\"Helvetica-Neue-Light 45\", 'verdana', helvetica, sans-serif", declarations['font-family']) end def test_getting_font_weight_from_shorthand ['300', 'bold', 'bolder', 'lighter', 'normal'].each do |unit| shorthand = "font: #{unit} italic 12px sans-serif;" declarations = expand_declarations(shorthand) assert_equal(unit, declarations['font-weight']) end # ensure normal is the default state ['font: normal italic 12px sans-serif;', 'font: italic 12px sans-serif;', 'font: small-caps normal 12px sans-serif;', 'font: 12px/16px sans-serif;'].each do |shorthand| declarations = expand_declarations(shorthand) assert_equal('normal', declarations['font-weight'], shorthand) end end def test_getting_font_variant_from_shorthand shorthand = "font: small-caps italic 12px sans-serif;" declarations = expand_declarations(shorthand) assert_equal('small-caps', declarations['font-variant']) # ensure normal is the default state ['font: normal italic 12px sans-serif;', 'font: italic 12px sans-serif;', 'font: normal 12px sans-serif;', 'font: 12px/16px sans-serif;'].each do |shorthand| declarations = expand_declarations(shorthand) assert_equal('normal', declarations['font-variant'], shorthand) end end def test_getting_font_style_from_shorthand ['italic', 'oblique'].each do |unit| shorthand = "font: normal #{unit} bold 12px sans-serif;" declarations = expand_declarations(shorthand) assert_equal(unit, declarations['font-style']) end # ensure normal is the default state ['font: normal bold 12px sans-serif;', 'font: small-caps 12px sans-serif;', 'font: normal 12px sans-serif;', 'font: 12px/16px sans-serif;'].each do |shorthand| declarations = expand_declarations(shorthand) assert_equal('normal', declarations['font-style'], shorthand) end end def test_getting_line_height_from_shorthand ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit| shorthand = "font: 300 italic 12px/0.25#{unit} verdana, helvetica, sans-serif;" declarations = expand_declarations(shorthand) assert_equal("0.25#{unit}", declarations['line-height']) end # ensure normal is the default state ['font: normal bold 12px sans-serif;', 'font: small-caps 12px sans-serif;', 'font: normal 12px sans-serif;', 'font: 12px sans-serif;'].each do |shorthand| declarations = expand_declarations(shorthand) assert_equal('normal', declarations['line-height'], shorthand) end end # Background shorthand def test_getting_background_properties_from_shorthand expected = {"background-image" => "url('chess.png')", "background-color" => "gray", "background-repeat" => "repeat", "background-attachment" => "fixed", "background-position" => "50%"} shorthand = "background: url('chess.png') gray 50% repeat fixed;" declarations = expand_declarations(shorthand) assert_equal expected, declarations end def test_getting_background_position_from_shorthand ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit| shorthand = "background: url('chess.png') gray 30% -0.15#{unit} repeat fixed;" declarations = expand_declarations(shorthand) assert_equal("30% -0.15#{unit}", declarations['background-position']) end ['left', 'center', 'right', 'top', 'bottom', 'inherit'].each do |position| shorthand = "background: url('chess.png') #000fff #{position} no-repeat fixed;" declarations = expand_declarations(shorthand) assert_equal(position, declarations['background-position']) end end def test_getting_background_colour_from_shorthand ['blue', 'lime', 'rgb(10,10,10)', 'rgb ( -10%, 99, 300)', '#ffa0a0', '#03c', 'trAnsparEnt', 'inherit'].each do |colour| shorthand = "background:#{colour} url('chess.png') center repeat fixed ;" declarations = expand_declarations(shorthand) assert_equal(colour, declarations['background-color']) end end def test_getting_background_attachment_from_shorthand ['scroll', 'fixed', 'inherit'].each do |attachment| shorthand = "background:#0f0f0f url('chess.png') center repeat #{attachment};" declarations = expand_declarations(shorthand) assert_equal(attachment, declarations['background-attachment']) end end def test_getting_background_repeat_from_shorthand ['repeat-x', 'repeat-y', 'no-repeat', 'inherit'].each do |repeat| shorthand = "background:#0f0f0f none #{repeat};" declarations = expand_declarations(shorthand) assert_equal(repeat, declarations['background-repeat']) end end def test_getting_background_image_from_shorthand ['url("chess.png")', 'url("https://example.org:80/~files/chess.png?123=abc&test#5")', 'url(https://example.org:80/~files/chess.png?123=abc&test#5)', "url('https://example.org:80/~files/chess.png?123=abc&test#5')", 'none', 'inherit'].each do |image| shorthand = "background: #0f0f0f #{image} ;" declarations = expand_declarations(shorthand) assert_equal(image, declarations['background-image']) end end def test_getting_background_gradient_from_shorthand ['linear-gradient(top, hsla(0, 0%, 0%, 0.00) 0%, hsla(0, 0%, 0%, 0.20) 100%)', '-webkit-gradient(linear, left top, left bottom, color-stop(0, hsla(0, 0%, 0%, 0.00)), color-stop(1, hsla(0, 0%, 0%, 0.20)))', '-moz-linear-gradient(bottom, blue, red)' ].each do |image| shorthand = "background: #0f0f0f #{image} repeat ;" declarations = expand_declarations(shorthand) assert_equal(image, declarations['background-image']) end end # List-style shorthand def test_getting_list_style_properties_from_shorthand expected = {'list-style-image' => 'url(\'chess.png\')', 'list-style-type' => 'katakana', 'list-style-position' => 'inside'} shorthand = "list-style: katakana inside url(\'chess.png\');" declarations = expand_declarations(shorthand) assert_equal expected, declarations end def test_getting_list_style_position_from_shorthand ['inside', 'outside'].each do |position| shorthand = "list-style: katakana #{position} url('chess.png');" declarations = expand_declarations(shorthand) assert_equal(position, declarations['list-style-position']) end end def test_getting_list_style_type_from_shorthand ['disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana', 'katakana', 'hira-gana-iroha', 'katakana-iroha', 'none'].each do |type| shorthand = "list-style: #{type} inside url('chess.png');" declarations = expand_declarations(shorthand) assert_equal(type, declarations['list-style-type']) end end protected def expand_declarations(declarations) ruleset = RuleSet.new(nil, declarations) ruleset.expand_shorthand! collected = {} ruleset.each_declaration do |prop, val, imp| collected[prop.to_s] = val.to_s end collected end end css-parser-1.3.6/test/test_rule_set_creating_shorthand.rb0000644000175000017500000001210212542615072024130 0ustar terceiroterceirorequire File.expand_path(File.dirname(__FILE__) + '/test_helper') # Test cases for reading and generating CSS shorthand properties class RuleSetCreatingShorthandTests < Test::Unit::TestCase include CssParser def setup @cp = CssParser::Parser.new end # Border shorthand def test_combining_borders_into_shorthand properties = {'border-top-width' => 'auto', 'border-right-width' => 'thin', 'border-bottom-width' => 'auto', 'border-left-width' => '0px'} combined = create_shorthand(properties) assert_equal('', combined['border']) assert_equal('auto thin auto 0px;', combined['border-width']) # after creating shorthand, all long-hand properties should be deleted assert_properties_are_deleted(combined, properties) # should not combine if any properties are missing properties.delete('border-top-width') combined = create_shorthand(properties) assert_equal '', combined['border-width'] properties = {'border-width' => '22%', 'border-color' => 'rgba(255, 0, 0)'} combined = create_shorthand(properties) assert_equal '22% rgba(255, 0, 0);', combined['border'] assert_equal '', combined['border-width'] properties = {'border-top-style' => 'none', 'border-right-style' => 'none', 'border-bottom-style' => 'none', 'border-left-style' => 'none'} combined = create_shorthand(properties) assert_equal 'none;', combined['border'] end # Dimensions shorthand def test_combining_dimensions_into_shorthand properties = {'margin-right' => 'auto', 'margin-bottom' => '0px', 'margin-left' => 'auto', 'margin-top' => '0px', 'padding-right' => '1.25em', 'padding-bottom' => '11%', 'padding-left' => '3pc', 'padding-top' => '11.25ex'} combined = create_shorthand(properties) assert_equal('0px auto;', combined['margin']) assert_equal('11.25ex 1.25em 11% 3pc;', combined['padding']) # after creating shorthand, all long-hand properties should be deleted assert_properties_are_deleted(combined, properties) # should not combine if any properties are missing properties.delete('margin-right') properties.delete('padding-right') combined = create_shorthand(properties) assert_equal '', combined['margin'] assert_equal '', combined['padding'] end # Dimensions shorthand, auto property def test_combining_dimensions_into_shorthand_with_auto rs = RuleSet.new('#page', "margin: 0; margin-left: auto; margin-right: auto;") rs.expand_shorthand! assert_equal('auto;', rs['margin-left']) rs.create_shorthand! assert_equal('0 auto;', rs['margin']) end # Font shorthand def test_combining_font_into_shorthand # should combine if all font properties are present properties = {"font-weight" => "300", "font-size" => "12pt", "font-family" => "sans-serif", "line-height" => "18px", "font-style" => "oblique", "font-variant" => "small-caps"} combined = create_shorthand(properties) assert_equal('oblique small-caps 300 12pt/18px sans-serif;', combined['font']) # after creating shorthand, all long-hand properties should be deleted assert_properties_are_deleted(combined, properties) # should not combine if any properties are missing properties.delete('font-weight') combined = create_shorthand(properties) assert_equal '', combined['font'] end # Background shorthand def test_combining_background_into_shorthand properties = {'background-image' => 'url(\'chess.png\')', 'background-color' => 'gray', 'background-position' => 'center -10.2%', 'background-attachment' => 'fixed', 'background-repeat' => 'no-repeat'} combined = create_shorthand(properties) assert_equal('gray url(\'chess.png\') no-repeat center -10.2% fixed;', combined['background']) # after creating shorthand, all long-hand properties should be deleted assert_properties_are_deleted(combined, properties) end # List-style shorthand def test_combining_list_style_into_shorthand properties = {'list-style-image' => 'url(\'chess.png\')', 'list-style-type' => 'katakana', 'list-style-position' => 'inside'} combined = create_shorthand(properties) assert_equal('katakana inside url(\'chess.png\');', combined['list-style']) # after creating shorthand, all long-hand properties should be deleted assert_properties_are_deleted(combined, properties) end def test_property_values_in_url rs = RuleSet.new('#header', "background:url(http://example.com/1528/www/top-logo.jpg) no-repeat top right; padding: 79px 0 10px 0; text-align:left;") rs.expand_shorthand! assert_equal('top right;', rs['background-position']) rs.create_shorthand! assert_equal('url(http://example.com/1528/www/top-logo.jpg) no-repeat top right;', rs['background']) end protected def assert_properties_are_deleted(ruleset, properties) properties.each do |property, value| assert_equal '', ruleset[property] end end def create_shorthand(properties) ruleset = RuleSet.new(nil, nil) properties.each do |property, value| ruleset[property] = value end ruleset.create_shorthand! ruleset end end css-parser-1.3.6/test/test_css_parser_misc.rb0000644000175000017500000001346712542615072021556 0ustar terceiroterceirorequire File.expand_path(File.dirname(__FILE__) + '/test_helper') # Test cases for the CssParser. class CssParserTests < Test::Unit::TestCase include CssParser def setup @cp = Parser.new end def test_at_page_rule # from http://www.w3.org/TR/CSS21/page.html#page-selectors css = <<-CSS @page { margin: 2cm } @page :first { margin-top: 10cm } CSS @cp.add_block!(css) assert_equal 'margin: 2cm;', @cp.find_by_selector('@page').join(' ') assert_equal 'margin-top: 10cm;', @cp.find_by_selector('@page :first').join(' ') end def test_should_ignore_comments # see http://www.w3.org/Style/CSS/Test/CSS2.1/current/html4/t040109-c17-comments-00-b.htm css =<<-CSS /* This is a CSS comment. */ .one {color: green;} /* Another comment */ /* The following should not be used: .one {color: red;} */ .two {color: green; /* color: yellow; */} /** .three {color: red;} */ .three {color: green;} /**/ .four {color: green;} /*********/ .five {color: green;} /* a comment **/ .six {color: green;} CSS @cp.add_block!(css) @cp.each_selector do |sel, decs, spec| assert_equal 'color: green;', decs end end def test_parsing_blocks # dervived from http://www.w3.org/TR/CSS21/syndata.html#rule-sets css = <<-CSS div[name='test'] { color: red; }div:hover{coloR:red; }div:first-letter{color:red;/*color:blue;}"commented out"*/} p[example="public class foo\ {\ private string x;\ \ foo(int x) {\ this.x = 'test';\ this.x = \"test\";\ }\ \ }"] { color: red } p { color:red} CSS @cp.add_block!(css) @cp.each_selector do |sel, decs, spec| assert_equal 'color: red;', decs end end def test_ignoring_malformed_declarations # dervived from http://www.w3.org/TR/CSS21/syndata.html#parsing-errors css = <<-CSS p { color:green } p { color:green; color } /* malformed declaration missing ':', value */ p { color:red; color; color:green } /* same with expected recovery */ p { color:green; color: } /* malformed declaration missing value */ p { color:red; color:; color:green } /* same with expected recovery */ p { color:green; color{;color:maroon} } /* unexpected tokens { } */ p { color:red; color{;color:maroon}; color:green } /* same with recovery */ CSS @cp.add_block!(css) @cp.each_selector do |sel, decs, spec| assert_equal 'color: green;', decs end end def test_find_rule_sets css = <<-CSS h1, h2 { color: blue; } h1 { font-size: 10px; } h2 { font-size: 5px; } CSS @cp.add_block!(css) assert_equal 2, @cp.find_rule_sets(["h2"]).size assert_equal 3, @cp.find_rule_sets(["h1", "h2"]).size end def test_calculating_specificity # from http://www.w3.org/TR/CSS21/cascade.html#specificity assert_equal 0, CssParser.calculate_specificity('*') assert_equal 1, CssParser.calculate_specificity('li') assert_equal 2, CssParser.calculate_specificity('li:first-line') assert_equal 2, CssParser.calculate_specificity('ul li') assert_equal 3, CssParser.calculate_specificity('ul ol+li') assert_equal 11, CssParser.calculate_specificity('h1 + *[rel=up]') assert_equal 13, CssParser.calculate_specificity('ul ol li.red') assert_equal 21, CssParser.calculate_specificity('li.red.level') assert_equal 100, CssParser.calculate_specificity('#x34y') # from http://www.hixie.ch/tests/adhoc/css/cascade/specificity/003.html assert_equal CssParser.calculate_specificity('div *'), CssParser.calculate_specificity('p') assert CssParser.calculate_specificity('body div *') > CssParser.calculate_specificity('div *') # other tests assert_equal 11, CssParser.calculate_specificity('h1[id|=123]') end def test_converting_uris base_uri = 'http://www.example.org/style/basic.css' ["body { background: url(yellow) };", "body { background: url('yellow') };", "body { background: url('/style/yellow') };", "body { background: url(\"../style/yellow\") };", "body { background: url(\"lib/../../style/yellow\") };"].each do |css| converted_css = CssParser.convert_uris(css, base_uri) assert_equal "body { background: url('http://www.example.org/style/yellow') };", converted_css end converted_css = CssParser.convert_uris("body { background: url(../style/yellow-dot_symbol$.png?abc=123&def=456&ghi=789#1011) };", base_uri) assert_equal "body { background: url('http://www.example.org/style/yellow-dot_symbol$.png?abc=123&def=456&ghi=789#1011') };", converted_css # taken from error log: 2007-10-23 04:37:41#2399 converted_css = CssParser.convert_uris('.specs {font-family:Helvetica;font-weight:bold;font-style:italic;color:#008CA8;font-size:1.4em;list-style-image:url("images/bullet.gif");}', 'http://www.example.org/directory/file.html') assert_equal ".specs {font-family:Helvetica;font-weight:bold;font-style:italic;color:#008CA8;font-size:1.4em;list-style-image:url('http://www.example.org/directory/images/bullet.gif');}", converted_css end def test_ruleset_with_braces =begin parser = Parser.new parser.add_block!("div { background-color: black !important; }") parser.add_block!("div { background-color: red; }") rulesets = [] parser['div'].each do |declaration| rulesets << RuleSet.new('div', declaration) end merged = CssParser.merge(rulesets) result: # merged.to_s => "{ background-color: black !important; }" =end new_rule = RuleSet.new('div', "{ background-color: black !important; }") assert_equal 'div { background-color: black !important; }', new_rule.to_s end end css-parser-1.3.6/test/test_merging.rb0000644000175000017500000000765412542615072020030 0ustar terceiroterceirorequire File.expand_path(File.dirname(__FILE__) + '/test_helper') class MergingTests < Test::Unit::TestCase include CssParser def setup @cp = CssParser::Parser.new end def test_simple_merge rs1 = RuleSet.new(nil, 'color: black;') rs2 = RuleSet.new(nil, 'margin: 0px;') merged = CssParser.merge(rs1, rs2) assert_equal '0px;', merged['margin'] assert_equal 'black;', merged['color'] end def test_merging_array rs1 = RuleSet.new(nil, 'color: black;') rs2 = RuleSet.new(nil, 'margin: 0px;') merged = CssParser.merge([rs1, rs2]) assert_equal '0px;', merged['margin'] assert_equal 'black;', merged['color'] end def test_merging_with_compound_selectors @cp.add_block! "body { margin: 0; }" @cp.add_block! "h2 { margin: 5px; }" rules = @cp.find_rule_sets([ "body", "h2" ]) assert_equal "margin: 5px;", CssParser.merge(rules).declarations_to_s @cp = CssParser::Parser.new @cp.add_block! "body { margin: 0; }" @cp.add_block! "h2,h1 { margin: 5px; }" rules = @cp.find_rule_sets([ "body", "h2" ]) assert_equal "margin: 5px;", CssParser.merge(rules).declarations_to_s end def test_merging_multiple rs1 = RuleSet.new(nil, 'color: black;') rs2 = RuleSet.new(nil, 'margin: 0px;') rs3 = RuleSet.new(nil, 'margin: 5px;') merged = CssParser.merge(rs1, rs2, rs3) assert_equal '5px;', merged['margin'] end def test_multiple_selectors_should_have_proper_specificity rs1 = RuleSet.new('p, a[rel="external"]', 'color: black;') rs2 = RuleSet.new('a', 'color: blue;') merged = CssParser.merge(rs1, rs2) assert_equal 'black;', merged['color'] end def test_setting_specificity rs1 = RuleSet.new(nil, 'color: red;', 20) rs2 = RuleSet.new(nil, 'color: blue;', 10) merged = CssParser.merge(rs1, rs2) assert_equal 'red;', merged['color'] end def test_properties_should_be_case_insensitive rs1 = RuleSet.new(nil, ' CoLor : red ;', 20) rs2 = RuleSet.new(nil, 'color: blue;', 10) merged = CssParser.merge(rs1, rs2) assert_equal 'red;', merged['color'] end def test_merging_backgrounds rs1 = RuleSet.new(nil, 'background-color: black;') rs2 = RuleSet.new(nil, 'background-image: none;') merged = CssParser.merge(rs1, rs2) assert_equal 'black none;', merged['background'] end def test_merging_dimensions rs1 = RuleSet.new(nil, 'margin: 3em;') rs2 = RuleSet.new(nil, 'margin-left: 1em;') merged = CssParser.merge(rs1, rs2) assert_equal '3em 3em 3em 1em;', merged['margin'] end def test_merging_fonts rs1 = RuleSet.new(nil, 'font: 11px Arial;') rs2 = RuleSet.new(nil, 'font-weight: bold;') merged = CssParser.merge(rs1, rs2) assert_equal 'bold 11px Arial;', merged['font'] end def test_raising_error_on_bad_type assert_raise ArgumentError do CssParser.merge([1,2,3]) end end def test_returning_early_with_only_one_params rs = RuleSet.new(nil, 'font-weight: bold;') merged = CssParser.merge(rs) assert_equal rs.object_id, merged.object_id end def test_merging_important rs1 = RuleSet.new(nil, 'color: black !important;') rs2 = RuleSet.new(nil, 'color: red;') merged = CssParser.merge(rs1, rs2) assert_equal 'black !important;', merged['color'] end def test_merging_multiple_important rs1 = RuleSet.new(nil, 'color: black !important;', 1000) rs2 = RuleSet.new(nil, 'color: red !important;', 1) merged = CssParser.merge(rs1, rs2) assert_equal 'black !important;', merged['color'] rs3 = RuleSet.new(nil, 'color: blue !important;', 1000) merged = CssParser.merge(rs1, rs2, rs3) assert_equal 'blue !important;', merged['color'] end def test_merging_shorthand_important rs1 = RuleSet.new(nil, 'background: black none !important;') rs2 = RuleSet.new(nil, 'background-color: red;') merged = CssParser.merge(rs1, rs2) assert_equal 'black !important;', merged['background-color'] end end css-parser-1.3.6/test/test_rule_set.rb0000644000175000017500000000672512542615072020220 0ustar terceiroterceirorequire File.expand_path(File.dirname(__FILE__) + '/test_helper') require "set" # Test cases for parsing CSS blocks class RuleSetTests < Test::Unit::TestCase include CssParser def setup @cp = Parser.new end def test_setting_property_values rs = RuleSet.new(nil, nil) rs['background-color'] = 'red' assert_equal('red;', rs['background-color']) rs['background-color'] = 'blue !important;' assert_equal('blue !important;', rs['background-color']) end def test_getting_property_values rs = RuleSet.new('#content p, a', 'color: #fff;') assert_equal('#fff;', rs['color']) end def test_getting_property_value_ignoring_case rs = RuleSet.new('#content p, a', 'color: #fff;') assert_equal('#fff;', rs[' ColoR ']) end def test_each_selector expected = [ {:selector => "#content p", :declarations => "color: #fff;", :specificity => 101}, {:selector => "a", :declarations => "color: #fff;", :specificity => 1} ] actual = [] rs = RuleSet.new('#content p, a', 'color: #fff;') rs.each_selector do |sel, decs, spec| actual << {:selector => sel, :declarations => decs, :specificity => spec} end assert_equal(expected, actual) end def test_each_declaration expected = Set.new([ {:property => 'margin', :value => '1px -0.25em', :is_important => false}, {:property => 'background', :value => 'white none no-repeat', :is_important => true}, {:property => 'color', :value => '#fff', :is_important => false} ]) actual = Set.new rs = RuleSet.new(nil, 'color: #fff; Background: white none no-repeat !important; margin: 1px -0.25em;') rs.each_declaration do |prop, val, imp| actual << {:property => prop, :value => val, :is_important => imp} end assert_equal(expected, actual) end def test_each_declaration_respects_order css_fragment = "margin: 0; padding: 20px; margin-bottom: 28px;" rs = RuleSet.new(nil, css_fragment) expected = %w(margin padding margin-bottom) actual = [] rs.each_declaration { |prop, val, imp| actual << prop } assert_equal(expected, actual) end def test_each_declaration_containing_semicolons rs = RuleSet.new(nil, "background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAiCAMAAAB7);" + "background-repeat: no-repeat") assert_equal('url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAiCAMAAAB7);', rs['background-image']) assert_equal('no-repeat;', rs['background-repeat']) end def test_selector_sanitization selectors = "h1, h2,\nh3 " rs = RuleSet.new(selectors, "color: #fff;") assert rs.selectors.member?("h3") end def test_multiple_selectors_to_s selectors = "#content p, a" rs = RuleSet.new(selectors, "color: #fff;") assert_match(/^\s*#content p,\s*a\s*\{/, rs.to_s) end def test_declarations_to_s declarations = 'color: #fff; font-weight: bold;' rs = RuleSet.new('#content p, a', declarations) assert_equal(declarations.split(' ').sort, rs.declarations_to_s.split(' ').sort) end def test_important_declarations_to_s declarations = 'color: #fff; font-weight: bold !important;' rs = RuleSet.new('#content p, a', declarations) assert_equal(declarations.split(' ').sort, rs.declarations_to_s.split(' ').sort) end def test_overriding_specificity rs = RuleSet.new('#content p, a', 'color: white', 1000) rs.each_selector do |sel, decs, spec| assert_equal 1000, spec end end end css-parser-1.3.6/Gemfile0000644000175000017500000000021212542615072015307 0ustar terceiroterceirosource "https://rubygems.org" gemspec gem 'rake' gem 'bump' gem 'test-unit', '>= 2.5.3' platforms :jruby do gem 'jruby-openssl' end css-parser-1.3.6/css_parser.gemspec0000644000175000017500000000072512542615072017536 0ustar terceiroterceiro$LOAD_PATH.unshift File.expand_path('../lib', __FILE__) name = "css_parser" require "#{name}/version" Gem::Specification.new name, CssParser::VERSION.dup do |s| s.summary = "Ruby CSS parser." s.description = "A set of classes for parsing CSS in Ruby." s.email = "code@dunae.ca" s.homepage = "https://github.com/premailer/#{name}" s.author = "Alex Dunae" s.add_runtime_dependency 'addressable' s.files = Dir.glob("lib/**/*") s.license = "MIT" end css-parser-1.3.6/Rakefile0000644000175000017500000000027612542615072015473 0ustar terceiroterceirorequire 'bundler/setup' require 'bundler/gem_tasks' require 'rake/testtask' require 'bump/tasks' desc 'Run the unit tests.' Rake::TestTask.new(:default) do |test| test.verbose = true end