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.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/Gemfile.lock +9 -1
- data/README.md +402 -2
- data/Rakefile +6 -0
- data/lib/gammo/attribute.rb +13 -4
- data/lib/gammo/attributes.rb +95 -0
- data/lib/gammo/node.rb +120 -26
- data/lib/gammo/parser.rb +3 -1
- data/lib/gammo/version.rb +1 -1
- data/lib/gammo/xpath.rb +74 -0
- data/lib/gammo/xpath/ast/axis.rb +231 -0
- data/lib/gammo/xpath/ast/expression.rb +250 -0
- data/lib/gammo/xpath/ast/function.rb +179 -0
- data/lib/gammo/xpath/ast/node_test.rb +86 -0
- data/lib/gammo/xpath/ast/path.rb +100 -0
- data/lib/gammo/xpath/ast/subclassify.rb +35 -0
- data/lib/gammo/xpath/ast/value.rb +150 -0
- data/lib/gammo/xpath/context.rb +23 -0
- data/lib/gammo/xpath/errors.rb +9 -0
- data/lib/gammo/xpath/node_set.rb +43 -0
- data/lib/gammo/xpath/parser.rb +1099 -0
- data/lib/gammo/xpath/parser.y +513 -0
- data/misc/table.erubi +1 -1
- metadata +16 -2
@@ -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
|