gammo 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,250 @@
1
+ require 'gammo/xpath/ast/value'
2
+
3
+ module Gammo
4
+ module XPath
5
+ module AST
6
+ # Class for representing a binary expression.
7
+ # @!visibility private
8
+ class BinaryExpr
9
+ # Constructs a binary expression by given "a" and "b".
10
+ # @param [Gammo::AST::Value, Gammo::AST:NodeSet] a
11
+ # @param [Gammo::AST::Value, Gammo::AST:NodeSet] b
12
+ # @!visibility private
13
+ def initialize(a, b)
14
+ @a = a
15
+ @b = b
16
+ end
17
+
18
+ # @!visibility private
19
+ def evaluate(context)
20
+ raise NotImplementedError, "BinaryExpr#evaluate must be implemented"
21
+ end
22
+
23
+ private
24
+
25
+ # @return [Array<Gammo::AST::Value...>]
26
+ # @!visibility private
27
+ def evaluate_values(context)
28
+ [@a.evaluate(context), @b.evaluate(context.dup)]
29
+ end
30
+ end
31
+
32
+ # Class for representing a binary expression that returns a boolean.
33
+ # @!visibility private
34
+ class BoolExpr < BinaryExpr
35
+ # @!visibility private
36
+ def evaluate(context)
37
+ compare(context, *evaluate_values(context))
38
+ end
39
+
40
+ private
41
+
42
+ # Compares both values and returns a boolean.
43
+ # @param [Context] context
44
+ # @param [Value] left
45
+ # @param [Value] right
46
+ # @return [TrueClass, FalseClass]
47
+ # @!visibility private
48
+ def compare(context, left, right)
49
+ return compare_with_node_set(
50
+ context, left.to_node_set(context), right) if left.node_set?
51
+ return compare_with_node_set(
52
+ context, right.to_node_set(context), left, reverse: true) if right.node_set?
53
+ do_compare(left, right)
54
+ end
55
+
56
+ # @!visibility private
57
+ def compare_with_node_set(context, node_set, value, reverse: false)
58
+ if value.node_set?
59
+ node_set.each do |lnode|
60
+ ls = string_from_node(lnode)
61
+ value.to_node_set(context).each do |rnode|
62
+ return true if compare(context, ls, string_from_node(rnode))
63
+ end
64
+ end
65
+ end
66
+ if value.number?
67
+ node_set.each do |node|
68
+ n = number_from_node(node)
69
+ return true if compare(context, *(reverse ? [value, n] : [n, value]))
70
+ end
71
+ return false
72
+ end
73
+ if value.string?
74
+ node_set.each do |node|
75
+ s = string_from_node(node)
76
+ return true if compare(context, *(reverse ? [value, s] : [s, value]))
77
+ end
78
+ return false
79
+ end
80
+ if value.bool?
81
+ b = node_set.to_bool
82
+ return compare(context, *(reverse ? [value, b] : [b, value]))
83
+ end
84
+ fail UnreachableError, 'unreachable pattern happens; please file an issue on github.'
85
+ end
86
+
87
+ # @!visibility private
88
+ def string_from_node(node)
89
+ case node
90
+ when Gammo::Node::Element, Gammo::Node::Document
91
+ AST::Value::String.new(node.inner_text)
92
+ when Gammo::Attribute
93
+ AST::Value::String.new(node.value)
94
+ when Gammo::Node::Comment, Gammo::Node::Text
95
+ AST::Value::String.new(node.data)
96
+ end
97
+ end
98
+
99
+ # @!visibility private
100
+ def number_from_node(node)
101
+ case node
102
+ when Gammo::Attribute
103
+ # TODO: Consider float case.
104
+ AST::Value::Number.new(node.value.to_i)
105
+ when Gammo::Node::Comment, Gammo::Node::Text
106
+ AST::Value::Number.new(node.data)
107
+ end
108
+ end
109
+
110
+ # @!visibility private
111
+ def equal?(left, right)
112
+ return left.to_bool == right.to_bool if left.bool? || right.bool?
113
+ return left.to_number == right.to_number if left.number? || right.number?
114
+ left.to_s == right.to_s
115
+ end
116
+ end
117
+
118
+ # @!visibility private
119
+ class EqExpr < BoolExpr
120
+ def do_compare(left, right)
121
+ equal?(left, right)
122
+ end
123
+ end
124
+
125
+ # @!visibility private
126
+ class NeqExpr < BoolExpr
127
+ def do_compare(left, right)
128
+ !equal?(left, right)
129
+ end
130
+ end
131
+
132
+ # @!visibility private
133
+ class LtExpr < BoolExpr
134
+ def do_compare(left, right)
135
+ left.to_number < right.to_number
136
+ end
137
+ end
138
+
139
+ # @!visibility private
140
+ class GtExpr < BoolExpr
141
+ def do_compare(left, right)
142
+ left.to_number > right.to_number
143
+ end
144
+ end
145
+
146
+ # @!visibility private
147
+ class LteExpr < BoolExpr
148
+ def do_compare(left, right)
149
+ left.to_number <= right.to_number
150
+ end
151
+ end
152
+
153
+ # @!visibility private
154
+ class GteExpr < BoolExpr
155
+ def do_compare(left, right)
156
+ left.to_number >= right.to_number
157
+ end
158
+ end
159
+
160
+ # Class for representing Arithmetic operators.
161
+ # @!visibility private
162
+ class ArithmeticExpr < BinaryExpr
163
+ def initialize(a, b)
164
+ super(a, b)
165
+ end
166
+
167
+ def evaluate(context)
168
+ # Expects left/right to be Integer.
169
+ Value::Number.new(do_arithmetic(*evaluate_values(context).map(&:to_number)))
170
+ end
171
+ end
172
+
173
+ # @!visibility private
174
+ class PlusExpr < ArithmeticExpr
175
+ def do_arithmetic(left, right)
176
+ left + right
177
+ end
178
+ end
179
+
180
+ # @!visibility private
181
+ class MinusExpr < ArithmeticExpr
182
+ def do_arithmetic(left, right)
183
+ left - right
184
+ end
185
+ end
186
+
187
+ # @!visibility private
188
+ class MultiplyExpr < ArithmeticExpr
189
+ def do_arithmetic(left, right)
190
+ left * right
191
+ end
192
+ end
193
+
194
+ # @!visibility private
195
+ class DividedExpr < ArithmeticExpr
196
+ def do_arithmetic(left, right)
197
+ left / right
198
+ end
199
+ end
200
+
201
+ # @!visibility private
202
+ class ModuloExpr < ArithmeticExpr
203
+ def do_arithmetic(left, right)
204
+ left % right
205
+ end
206
+ end
207
+
208
+ # @!visibility private
209
+ class UnionExpr < BinaryExpr
210
+ def evaluate(context)
211
+ cloned = context.clone
212
+ left, right = @a.evaluate(context), @b.evaluate(cloned)
213
+ left_node_set = left.to_node_set(context)
214
+ right_node_set = right.to_node_set(cloned)
215
+
216
+ duplicates = Set.new(left_node_set.nodes)
217
+ right_node_set.each { |node| left_node_set << node if duplicates.add?(node) }
218
+ left
219
+ end
220
+ end
221
+
222
+ # @!visibility private
223
+ class Negative
224
+ def initialize(expression)
225
+ @expression = expression
226
+ end
227
+
228
+ def evaluate(context)
229
+ AST::Value::Number.new(-@expression.evaluate(context).to_number)
230
+ end
231
+ end
232
+
233
+ # Class for representing predicate like "[foo='bar']" and "[0]".
234
+ # @!visibility private
235
+ class Predicate
236
+ def initialize(value)
237
+ @value = value
238
+ end
239
+
240
+ def evaluate(context)
241
+ ret = @value.evaluate(context)
242
+ if ret.instance_of?(AST::Value::Number)
243
+ return EqExpr.new(AST::Function::Position.new, ret).evaluate(context)
244
+ end
245
+ ret
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,179 @@
1
+ require 'gammo/xpath/ast/subclassify'
2
+ require 'gammo/xpath/ast/value'
3
+
4
+ module Gammo
5
+ module XPath
6
+ module AST
7
+ # Class for representing XPath core function library.
8
+ # https://www.w3.org/TR/1999/REC-xpath-19991116/#corelib
9
+ class Function
10
+ extend Subclassify
11
+
12
+ # @!visibility private
13
+ def initialize(*arguments)
14
+ @arguments = arguments
15
+ end
16
+
17
+ # @!visibility private
18
+ def evaluate(context)
19
+ raise NotImplementedError, '#evaluate must be implemented'
20
+ end
21
+
22
+ private
23
+
24
+ # @!visibility private
25
+ def number(val)
26
+ return val if val.instance_of?(Value::Number)
27
+ Value::Number.new(val)
28
+ end
29
+
30
+ # @!visibility private
31
+ def bool(val)
32
+ return val if val.instance_of?(Value::Boolean)
33
+ Value::Boolean.new(val)
34
+ end
35
+
36
+ # @!visibility private
37
+ def string(val)
38
+ return val if val.instance_of?(Value::String)
39
+ Value::String.new(val)
40
+ end
41
+
42
+ attr_reader :arguments
43
+
44
+ # @!visibility private
45
+ class Boolean < Function
46
+ declare :boolean
47
+
48
+ def evaluate(context)
49
+ bool arguments[0].evaluate(context)
50
+ end
51
+ end
52
+
53
+ # @!visibility private
54
+ class Not < Function
55
+ declare :not
56
+
57
+ def evaluate(context)
58
+ bool !arguments[0].evaluate(context)
59
+ end
60
+ end
61
+
62
+ # @!visibility private
63
+ class True < Function
64
+ declare :true
65
+
66
+ def evaluate(context)
67
+ bool true
68
+ end
69
+ end
70
+
71
+ # @!visibility private
72
+ class False < Function
73
+ declare :false
74
+
75
+ def evaluate(context)
76
+ bool false
77
+ end
78
+ end
79
+
80
+ # @!visibility private
81
+ class Ceiling < Function
82
+ declare :ceiling
83
+
84
+ def evaluate(context)
85
+ number arguments[0].evaluate(context).value.ceil
86
+ end
87
+ end
88
+
89
+ # @!visibility private
90
+ class String < Function
91
+ declare :string
92
+
93
+ def evaluate(context)
94
+ return string context.node.to_s if arguments.length.zero?
95
+ string arguments[0].evaluate(context).to_s
96
+ end
97
+ end
98
+
99
+ # @!visibility private
100
+ class Concat < Function
101
+ declare :concat
102
+
103
+ def evaluate(context)
104
+ string arguments.each_with_object(::String.new) { |argument, s|
105
+ s << argument.evaluate(context.clone).to_s
106
+ }
107
+ end
108
+ end
109
+
110
+ # @!visibility private
111
+ class StartsWith < Function
112
+ declare :'starts-with'
113
+
114
+ def evaluate(context)
115
+ s1 = arguments[0].evaluate(context).to_s
116
+ s2 = arguments[1].evaluate(context.clone).to_s
117
+ return bool(true) if s2.empty?
118
+ bool s1.start_with?(s2)
119
+ end
120
+ end
121
+
122
+ # @!visibility private
123
+ class Contains < Function
124
+ declare :contains
125
+
126
+ def evaluate(context)
127
+ substr = arguments[1].evaluate(context).to_s
128
+ return bool(true) if substr.empty?
129
+ bool arguments[0].evaluate(context).to_s.include?(substr)
130
+ end
131
+ end
132
+
133
+ # @!visibility private
134
+ class SubstringBefore < Function
135
+ declare :'substring-before'
136
+
137
+ def evaluate(context)
138
+ s1 = arguments[0].evaluate(context).to_s
139
+ s2 = arguments[1].evaluate(context.clone).to_s
140
+ return string '' if s2.empty?
141
+ return string '' unless pos = s1.index(s2)
142
+ string s1[0...pos]
143
+ end
144
+ end
145
+
146
+ # @!visibility private
147
+ class SubstringAfter < Function
148
+ declare :'substring-after'
149
+
150
+ def evaluate(context)
151
+ s1 = arguments[0].evaluate(context).to_s
152
+ s2 = arguments[1].evaluate(context.clone).to_s
153
+ return string '' if s2.empty?
154
+ return string '' unless pos = s1.rindex(s2)
155
+ string s1[(pos + s2.length)..-1]
156
+ end
157
+ end
158
+
159
+ # @!visibility private
160
+ class Last < Function
161
+ declare :last
162
+
163
+ def evaluate(context)
164
+ number context.size
165
+ end
166
+ end
167
+
168
+ # @!visibility private
169
+ class Position < Function
170
+ declare :position
171
+
172
+ def evaluate(context)
173
+ number context.position
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,86 @@
1
+ require 'gammo/xpath/ast/subclassify'
2
+ require 'gammo/xpath/errors'
3
+
4
+ module Gammo
5
+ module XPath
6
+ module AST
7
+ # @!visibility private
8
+ class NodeTest
9
+ extend Subclassify
10
+
11
+ def match?(node)
12
+ fail NotImplementedError, "#match must be implemented"
13
+ end
14
+
15
+ class Name < NodeTest
16
+ declare :name
17
+
18
+ attr_reader :local, :namespace
19
+
20
+ def initialize(local: nil, namespace: nil)
21
+ @local = local
22
+ @namespace = namespace
23
+ end
24
+
25
+ def xml_namespace?
26
+ namespace == 'http://www.w3.org/XML/1998/namespace'
27
+ end
28
+
29
+ def match?(node)
30
+ return false unless node
31
+ return false if xml_namespace?
32
+ return !namespace || namespace == node.namespace if local == ?*
33
+ # TODO: investigate
34
+ if node.instance_of?(Gammo::Attribute)
35
+ # TODO: need to work
36
+ node.key == local && node.namespace == namespace
37
+ else
38
+ if document = node.owner_document
39
+ # TODO: ignoring ascii case
40
+ return node.tag == local && (!namespace || node.namespace == namespace) if node.instance_of?(Gammo::Node::Element)
41
+ return node.tag == local && node.namespace == namespace && namespace
42
+ end
43
+ node.tag == local && node.namespace == namespace
44
+ end
45
+ end
46
+ end
47
+
48
+ # @!visibility private
49
+ class Any < NodeTest
50
+ declare :node
51
+
52
+ def match?(node)
53
+ true
54
+ end
55
+ end
56
+
57
+ # @!visibility private
58
+ class Text < NodeTest
59
+ declare :text
60
+
61
+ def match?(node)
62
+ node.instance_of?(Gammo::Node::Text)
63
+ end
64
+ end
65
+
66
+ # @!visibility private
67
+ class Comment < NodeTest
68
+ declare :comment
69
+
70
+ def match?(node)
71
+ node.instance_of?(Gammo::Node::Comment)
72
+ end
73
+ end
74
+
75
+ # @!visibility private
76
+ class ProcessingInstruction < NodeTest
77
+ declare :'processing-instruction'
78
+
79
+ def initialize
80
+ fail NotImplementedError, 'processing-instruction is not supported'
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end