class Prawn::SVG::CSS::SelectorParser

Constants

Attribute
Combinator
Identifier
Modifier
VALID_CSS_IDENTIFIER_CHAR

Public Class Methods

parse(selector) click to toggle source
# File lib/prawn/svg/css/selector_parser.rb, line 3
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
tokenise_css_selector(selector) click to toggle source
# File lib/prawn/svg/css/selector_parser.rb, line 37
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 [' ', "\t"].include?(char)
          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 [' ', "\t"].include?(char)
          attribute = :pre_value
        elsif ['"', "'"].include?(char)
          result.last.value = +''
          attribute = String.new(char)
        else
          result.last.value = String.new(char)
          attribute = :value
        end

      when :pre_value
        if ['"', "'"].include?(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) unless 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)
        unless result.last.is_a?(Identifier) && result[-2] && result[-2].is_a?(Modifier) && result[-2].type == :pseudo_class
          return
        end

        result.last.name << '('
        brackets = true
      when '['
        result << Attribute.new
        attribute = :pre_key
      else
        return # unsupported Combinator
      end
    end
  end

  result unless brackets || attribute
end