Module: Taurus::XML::CssToXPath

Defined in:
lib/taurus/xml/css_to_xpath.rb

Overview

Minimal CSS-to-XPath translator covering the common Nokogiri subset:

tag                    → //tag
*                      → //*
.class                 → //*[contains(concat(' ',@class,' '),' class ')]
#id                    → //*[@id='id']
[attr]                 → //*[@attr]
[attr=val]             → //*[@attr='val']
tag[attr]              → //tag[@attr]
tag[attr=val]          → //tag[@attr='val']
parent > child         → //parent/child
ancestor descendant    → //ancestor//descendant
a, b                   → //a | //b
tag:first-child        → //tag[position()=1]
tag:last-child         → //tag[position()=last()]
tag:not(simple)        → //tag[not(self::simple)]

For anything beyond this subset, raise ArgumentError. Callers can fall back to writing XPath directly via #xpath/#at_xpath.

Constant Summary collapse

TAG_RE =

Token types in a single simple selector. tag (optional), then zero or more of: .class, #id, [attr...], :pseudo

/\A(\*|[\w-]+)/.freeze
DOT_CLASS_RE =
/\A\.([\w-]+)/.freeze
HASH_ID_RE =
/\A#([\w-]+)/.freeze
BRACKET_RE =
/\A\[([^\]]+)\]/.freeze
PSEUDO_RE =
/\A:([\w-]+(?:\([^)]*\))?)/.freeze

Class Method Summary collapse

Class Method Details

.convert(rule) ⇒ Object



38
39
40
# File 'lib/taurus/xml/css_to_xpath.rb', line 38

def convert(rule)
  rule.to_s.split(COMMA_SPLIT).map { |r| convert_one(r.strip) }.join(" | ")
end

.convert_one(rule) ⇒ Object



42
43
44
45
46
47
48
49
50
51
# File 'lib/taurus/xml/css_to_xpath.rb', line 42

def convert_one(rule)
  return "//*" if rule == "*"

  # Tokenize chain first (handles > and whitespace)
  if rule =~ /\s/ || rule.include?(">")
    return convert_chain(rule)
  end

  convert_simple(rule, prefix: "//")
end

.parse_simple(part) ⇒ Object

Parse a single simple selector into (tag, predicates) where predicates is an array of XPath fragments to be joined via [p1][p2]...



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/taurus/xml/css_to_xpath.rb', line 55

def parse_simple(part)
  tag = "*"
  preds = []

  s = part.strip
  if s =~ TAG_RE
    tag = $1
    s = $'
  end

  until s.empty?
    case s
    when DOT_CLASS_RE
      preds << "contains(concat(' ',normalize-space(@class),' '),' #{$1} ')"
      s = $'
    when HASH_ID_RE
      preds << "@id='#{$1}'"
      s = $'
    when BRACKET_RE
      preds << convert_attrs($1)
      s = $'
    when PSEUDO_RE
      preds << convert_pseudo($1)
      s = $'
    else
      raise ArgumentError,
        "unsupported CSS selector at #{s.inspect} (in #{part.inspect})"
    end
  end

  [tag, preds]
end