File: renderer.rb

package info (click to toggle)
ruby-xpath 3.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, buster, forky, sid, trixie
  • size: 140 kB
  • sloc: ruby: 847; makefile: 2
file content (126 lines) | stat: -rw-r--r-- 2,767 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# frozen_string_literal: true

module XPath
  class Renderer
    def self.render(node, type)
      new(type).render(node)
    end

    def initialize(type)
      @type = type
    end

    def render(node)
      arguments = node.arguments.map { |argument| convert_argument(argument) }
      send(node.expression, *arguments)
    end

    def convert_argument(argument)
      case argument
      when Expression, Union then render(argument)
      when Array then argument.map { |element| convert_argument(element) }
      when String then string_literal(argument)
      when Literal then argument.value
      else argument.to_s
      end
    end

    def string_literal(string)
      if string.include?("'")
        string = string.split("'", -1).map do |substr|
          "'#{substr}'"
        end.join(%q(,"'",))
        "concat(#{string})"
      else
        "'#{string}'"
      end
    end

    def this_node
      '.'
    end

    def descendant(current, element_names)
      with_element_conditions("#{current}//", element_names)
    end

    def child(current, element_names)
      with_element_conditions("#{current}/", element_names)
    end

    def axis(current, name, element_names)
      with_element_conditions("#{current}/#{name}::", element_names)
    end

    def anywhere(element_names)
      with_element_conditions('//', element_names)
    end

    def where(on, condition)
      "#{on}[#{condition}]"
    end

    def attribute(current, name)
      if valid_xml_name?(name)
        "#{current}/@#{name}"
      else
        "#{current}/attribute::*[local-name(.) = #{string_literal(name)}]"
      end
    end

    def binary_operator(name, left, right)
      "(#{left} #{name} #{right})"
    end

    def is(one, two)
      if @type == :exact
        binary_operator('=', one, two)
      else
        function(:contains, one, two)
      end
    end

    def variable(name)
      "%{#{name}}"
    end

    def text(current)
      "#{current}/text()"
    end

    def literal(node)
      node
    end

    def css(current, selector)
      paths = Nokogiri::CSS.xpath_for(selector).map do |xpath_selector|
        "#{current}#{xpath_selector}"
      end
      union(paths)
    end

    def union(*expressions)
      expressions.join(' | ')
    end

    def function(name, *arguments)
      "#{name}(#{arguments.join(', ')})"
    end

  private

    def with_element_conditions(expression, element_names)
      if element_names.length == 1
        "#{expression}#{element_names.first}"
      elsif element_names.length > 1
        "#{expression}*[#{element_names.map { |e| "self::#{e}" }.join(' | ')}]"
      else
        "#{expression}*"
      end
    end

    def valid_xml_name?(name)
      name =~ /^[a-zA-Z_:][a-zA-Z0-9_:\.\-]*$/
    end
  end
end