module Prawn::SVG::CSS class SelectorParser def self.parse(selector) tokens = tokenise_css_selector(selector) or return result = [{}] part = nil tokens.each do |token| case token when Modifier part = token.type result.last[part] ||= part == :name ? +"" : [] when Identifier return unless part result.last[part] << token.name when Attribute return unless ["=", "*=", "~=", "^=", "|=", "$=", nil].include?(token.operator) (result.last[:attribute] ||= []) << [token.key, token.operator, token.value] when Combinator result << {combinator: token.type} part = nil end end result end private VALID_CSS_IDENTIFIER_CHAR = /[a-zA-Z0-9_\u00a0-\uffff-]/ Identifier = Struct.new(:name) Modifier = Struct.new(:type) Combinator = Struct.new(:type) Attribute = Struct.new(:key, :operator, :value) def self.tokenise_css_selector(selector) result = [] brackets = false attribute = false quote = false selector.strip.chars do |char| if brackets result.last.name << char brackets = false if char == ')' elsif attribute case attribute when :pre_key if VALID_CSS_IDENTIFIER_CHAR.match(char) result.last.key = String.new(char) attribute = :key elsif char != " " && char != "\t" return end when :key if VALID_CSS_IDENTIFIER_CHAR.match(char) result.last.key << char elsif char == "]" attribute = nil elsif "=*~^|$".include?(char) result.last.operator = String.new(char) attribute = :operator elsif char == " " || char == "\t" attribute = :pre_operator else return end when :pre_operator if "=*~^|$".include?(char) result.last.operator = String.new(char) attribute = :operator elsif char != " " && char != "\t" return end when :operator if "=*~^|$".include?(char) result.last.operator << char elsif char == " " || char == "\t" attribute = :pre_value elsif char == '"' || char == "'" result.last.value = +'' attribute = String.new(char) else result.last.value = String.new(char) attribute = :value end when :pre_value if char == '"' || char == "'" result.last.value = +'' attribute = String.new(char) elsif char != " " && char != "\t" result.last.value = String.new(char) attribute = :value end when :value if char == "]" result.last.value = String.new(result.last.value.rstrip) attribute = nil else result.last.value << char end when '"', "'" if char == "\\" && !quote quote = true elsif char == attribute && !quote attribute = :post_string else quote = false result.last.value << char end when :post_string if char == "]" attribute = nil elsif char != " " && char != "\t" return end end elsif VALID_CSS_IDENTIFIER_CHAR.match(char) case result.last when Identifier result.last.name << char else result << Modifier.new(:name) if !result.last.is_a?(Modifier) result << Identifier.new(char) end else case char when "." result << Modifier.new(:class) when "#" result << Modifier.new(:id) when ":" result << Modifier.new(:pseudo_class) when " ", "\t" result << Combinator.new(:descendant) unless result.last.is_a?(Combinator) when ">" result.pop if result.last == Combinator.new(:descendant) result << Combinator.new(:child) when "+" result.pop if result.last == Combinator.new(:descendant) result << Combinator.new(:adjacent) when "~" result.pop if result.last == Combinator.new(:descendant) result << Combinator.new(:siblings) when "*" return unless result.empty? || result.last.is_a?(Combinator) result << Modifier.new(:name) result << Identifier.new("*") when "(" # e.g. :nth-child(3n+4) return unless result.last.is_a?(Identifier) && result[-2] && result[-2].is_a?(Modifier) && result[-2].type == :pseudo_class result.last.name << "(" brackets = true when "[" result << Attribute.new attribute = :pre_key else return # unsupported Combinator end end end result unless brackets || attribute end end end