synvert-core 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/synvert/core/array_ext.rb +9 -2
- data/lib/synvert/core/node_query/compiler/array.rb +1 -1
- data/lib/synvert/core/node_query/compiler/expression.rb +69 -31
- data/lib/synvert/core/node_query/compiler/selector.rb +17 -14
- data/lib/synvert/core/node_query/lexer.rex +8 -1
- data/lib/synvert/core/node_query/lexer.rex.rb +20 -2
- data/lib/synvert/core/node_query/parser.racc.rb +221 -247
- data/lib/synvert/core/node_query/parser.y +9 -12
- data/lib/synvert/core/version.rb +1 -1
- data/spec/synvert/core/node_query/lexer_spec.rb +30 -4
- data/spec/synvert/core/node_query/parser_spec.rb +54 -19
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5bf36b2f9e986dd2baf0bbddf53f4671a3bf98af0db830c270960eb583b47ce
|
4
|
+
data.tar.gz: 7e8693858d73d11c09652076ee03d49fe69662386caff67cbd0a3cc37e47d0a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad0eb6125d82241c60f7f0e229b9735f7c253280f1e56d586dfd24e8afb9c25b2cf574ca9c21e252c485f529b1bdf5f6b26c2b59805781cdefd3bc07a76072d8
|
7
|
+
data.tar.gz: 37bd754087a17d55577a10338fef9da3c60733d8634e9c394666a826fe205dc7f895121570f1e9cca4e7e1284f0b3b11c16e532e197afe2cd0ce61424c5d1557
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## 1.2.0 (2022-04-29)
|
4
|
+
|
5
|
+
* Remove comma in NQL array value
|
6
|
+
* Parse pseduo class without selector in NQL
|
7
|
+
* Parse multiple goto scope in NQL
|
8
|
+
* Parse `nil?` in NQL
|
9
|
+
|
3
10
|
## 1.1.1 (2022-04-27)
|
4
11
|
|
5
12
|
* Parse empty string properly in node query language
|
@@ -13,7 +13,7 @@ class Array
|
|
13
13
|
return child_direct_child_node if child_direct_child_node
|
14
14
|
|
15
15
|
raise Synvert::Core::MethodNotSupported,
|
16
|
-
"child_node_by_name is not handled for #{
|
16
|
+
"child_node_by_name is not handled for #{debug_info}, child_name: #{child_name}"
|
17
17
|
end
|
18
18
|
|
19
19
|
# Get the source range of child node.
|
@@ -35,7 +35,14 @@ class Array
|
|
35
35
|
)
|
36
36
|
else
|
37
37
|
raise Synvert::Core::MethodNotSupported,
|
38
|
-
"child_node_range is not handled for #{
|
38
|
+
"child_node_range is not handled for #{debug_info}, child_name: #{child_name}"
|
39
39
|
end
|
40
40
|
end
|
41
|
+
|
42
|
+
# Return the debug info.
|
43
|
+
#
|
44
|
+
# @return [String] file, line, source and node.
|
45
|
+
def debug_info
|
46
|
+
map(&:debug_info).join("\n")
|
47
|
+
end
|
41
48
|
end
|
@@ -5,10 +5,12 @@ module Synvert::Core::NodeQuery::Compiler
|
|
5
5
|
class Expression
|
6
6
|
# Initialize a Expression.
|
7
7
|
# @param selector [Synvert::Core::NodeQuery::Compiler::Selector] the selector
|
8
|
+
# @param goto_scope [String] goto scope
|
8
9
|
# @param rest [Synvert::Core::NodeQuery::Compiler::Expression] the rest expression
|
9
10
|
# @param relationship [Symbol] the relationship between the selector and rest expression, it can be <code>:descendant</code>, <code>:child</code>, <code>:next_sibling</code>, <code>:subsequent_sibling</code> or <code>nil</code>.
|
10
|
-
def initialize(selector: nil, rest: nil, relationship: nil)
|
11
|
+
def initialize(selector: nil, goto_scope: nil, rest: nil, relationship: nil)
|
11
12
|
@selector = selector
|
13
|
+
@goto_scope = goto_scope
|
12
14
|
@rest = rest
|
13
15
|
@relationship = relationship
|
14
16
|
end
|
@@ -30,49 +32,85 @@ module Synvert::Core::NodeQuery::Compiler
|
|
30
32
|
# @param node [Parser::AST::Node] node to match
|
31
33
|
# @param descendant_match [Boolean] whether to match in descendant node
|
32
34
|
# @return [Array<Parser::AST::Node>] matching nodes.
|
33
|
-
def query_nodes(node, descendant_match
|
34
|
-
|
35
|
-
if @relationship.nil?
|
36
|
-
return matching_nodes
|
37
|
-
end
|
35
|
+
def query_nodes(node, descendant_match = true)
|
36
|
+
return find_nodes_by_goto_scope(node) if @goto_scope
|
38
37
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
nodes
|
47
|
-
when :child
|
48
|
-
matching_node.children.map { |child_node| @rest.query_nodes(child_node, descendant_match: false) }
|
49
|
-
.flatten
|
50
|
-
when :next_sibling
|
51
|
-
@rest.query_nodes(matching_node.siblings.first, descendant_match: false)
|
52
|
-
when :subsequent_sibling
|
53
|
-
matching_node.siblings.map { |sibling_node| @rest.query_nodes(sibling_node, descendant_match: false) }
|
54
|
-
.flatten
|
55
|
-
end
|
56
|
-
end.flatten
|
38
|
+
return find_nodes_by_relationship(node) if @relationship
|
39
|
+
|
40
|
+
matching_nodes = find_nodes_without_relationship(node, descendant_match)
|
41
|
+
return matching_nodes if @rest.nil?
|
42
|
+
|
43
|
+
matching_nodes.map { |matching_node| find_nodes_by_rest(matching_node, descendant_match) }
|
44
|
+
.flatten
|
57
45
|
end
|
58
46
|
|
59
47
|
def to_s
|
60
48
|
return @selector.to_s unless @rest
|
61
49
|
|
62
50
|
result = []
|
63
|
-
result << @selector if @selector
|
51
|
+
result << @selector.to_s if @selector
|
52
|
+
result << "<#{@goto_scope}>" if @goto_scope
|
64
53
|
case @relationship
|
65
|
-
when :child then result <<
|
66
|
-
when :subsequent_sibling then result <<
|
67
|
-
when :next_sibling then result <<
|
54
|
+
when :child then result << "> #{@rest}"
|
55
|
+
when :subsequent_sibling then result << "~ #{@rest}"
|
56
|
+
when :next_sibling then result << "+ #{@rest}"
|
57
|
+
when :has then result << ":has(#{@rest})"
|
58
|
+
when :not_has then result << ":not_has(#{@rest})"
|
59
|
+
else result << @rest.to_s
|
68
60
|
end
|
69
|
-
result
|
70
|
-
result.map(&:to_s).join(' ')
|
61
|
+
result.join(' ')
|
71
62
|
end
|
72
63
|
|
73
64
|
private
|
74
65
|
|
75
|
-
|
66
|
+
# Find nodes by @goto_scope
|
67
|
+
# @param node [Parser::AST::Node] node to match
|
68
|
+
def find_nodes_by_goto_scope(node)
|
69
|
+
@goto_scope.split('.').each { |scope| node = node.send(scope) }
|
70
|
+
@rest.query_nodes(node, false)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Find ndoes by @relationship
|
74
|
+
# @param node [Parser::AST::Node] node to match
|
75
|
+
def find_nodes_by_relationship(node)
|
76
|
+
case @relationship
|
77
|
+
when :child
|
78
|
+
if node.is_a?(::Array)
|
79
|
+
return node.map { |each_node| find_nodes_by_rest(each_node) }
|
80
|
+
.flatten
|
81
|
+
else
|
82
|
+
return node.children.map { |each_node| find_nodes_by_rest(each_node) }
|
83
|
+
.flatten
|
84
|
+
end
|
85
|
+
when :next_sibling
|
86
|
+
return find_nodes_by_rest(node.siblings.first)
|
87
|
+
when :subsequent_sibling
|
88
|
+
return node.siblings.map { |each_node| find_nodes_by_rest(each_node) }
|
89
|
+
.flatten
|
90
|
+
when :has
|
91
|
+
return @rest.match?(node) ? [node] : []
|
92
|
+
when :not_has
|
93
|
+
return !@rest.match?(node) ? [node] : []
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Find nodes by @rest
|
98
|
+
# @param node [Parser::AST::Node] node to match
|
99
|
+
# @param descendant_match [Boolean] whether to match in descendant node
|
100
|
+
def find_nodes_by_rest(node, descendant_match = false)
|
101
|
+
@rest.query_nodes(node, descendant_match)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Find nodes with nil relationship.
|
105
|
+
# @param node [Parser::AST::Node] node to match
|
106
|
+
# @param descendant_match [Boolean] whether to match in descendant node
|
107
|
+
def find_nodes_without_relationship(node, descendant_match = true)
|
108
|
+
if node.is_a?(::Array)
|
109
|
+
return node.map { |each_node|
|
110
|
+
find_nodes_without_relationship(each_node, descendant_match)
|
111
|
+
}.flatten
|
112
|
+
end
|
113
|
+
|
76
114
|
return [node] unless @selector
|
77
115
|
|
78
116
|
nodes = []
|
@@ -33,21 +33,24 @@ module Synvert::Core::NodeQuery::Compiler
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def to_s
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
36
|
+
result = []
|
37
|
+
result << ".#{@node_type}" if @node_type
|
38
|
+
result << @attribute_list.to_s if @attribute_list
|
39
|
+
result << ":#{@pseudo_class}(#{@pseudo_expression})" if @pseudo_class
|
40
|
+
if @index
|
41
|
+
result <<
|
42
|
+
case @index
|
43
|
+
when 0
|
44
|
+
':first-child'
|
45
|
+
when -1
|
46
|
+
':last-child'
|
47
|
+
when (1..)
|
48
|
+
":nth-child(#{@index + 1})"
|
49
|
+
else # ...-1
|
50
|
+
":nth-last-child(#{-@index})"
|
51
|
+
end
|
50
52
|
end
|
53
|
+
result.join('')
|
51
54
|
end
|
52
55
|
end
|
53
56
|
end
|
@@ -7,6 +7,8 @@ macros
|
|
7
7
|
CLOSE_ARRAY /\)/
|
8
8
|
OPEN_SELECTOR /\(/
|
9
9
|
CLOSE_SELECTOR /\)/
|
10
|
+
OPEN_GOTO_SCOPE /</
|
11
|
+
CLOSE_GOTO_SCOPE />/
|
10
12
|
OPEN_DYNAMIC_ATTRIBUTE /{{/
|
11
13
|
CLOSE_DYNAMIC_ATTRIBUTE /}}/
|
12
14
|
NODE_TYPE /\.[a-z]+/
|
@@ -39,7 +41,11 @@ rules
|
|
39
41
|
/\+/ { [:tNEXT_SIBLING, text] }
|
40
42
|
/#{OPEN_SELECTOR}/ { [:tOPEN_SELECTOR, text] }
|
41
43
|
/#{CLOSE_SELECTOR}/ { [:tCLOSE_SELECTOR, text] }
|
44
|
+
/#{OPEN_GOTO_SCOPE}/ { @state = :GOTO_SCOPE; [:tOPEN_GOTO_SCOPE, text] }
|
42
45
|
/#{OPEN_ATTRIBUTE}/ { @nested_count += 1; @state = :KEY; [:tOPEN_ATTRIBUTE, text] }
|
46
|
+
:GOTO_SCOPE /\s+/
|
47
|
+
:GOTO_SCOPE /#{IDENTIFIER}/ { [:tIDENTIFIER, text] }
|
48
|
+
:GOTO_SCOPE /#{CLOSE_GOTO_SCOPE}/ { @state = nil; [:tCLOSE_GOTO_SCOPE, text] }
|
43
49
|
:KEY /\s+/
|
44
50
|
:KEY /!=/ { @state = :VALUE; [:tNOT_EQUAL, text] }
|
45
51
|
:KEY /=~/ { @state = :VALUE; [:tMATCH, text] }
|
@@ -61,6 +67,7 @@ rules
|
|
61
67
|
:VALUE /#{OPEN_DYNAMIC_ATTRIBUTE}/ { @state = :DYNAMIC_ATTRIBUTE; [:tOPEN_DYNAMIC_ATTRIBUTE, text] }
|
62
68
|
:VALUE /#{OPEN_ARRAY}/ { @state = :ARRAY_VALUE; [:tOPEN_ARRAY, text] }
|
63
69
|
:VALUE /#{CLOSE_ATTRIBUTE}/ { @nested_count -= 1; @state = @nested_count == 0 ? nil : :VALUE; [:tCLOSE_ATTRIBUTE, text] }
|
70
|
+
:VALUE /#{NIL}\?/ { [:tIDENTIFIER_VALUE, text] }
|
64
71
|
:VALUE /#{NIL}/ { [:tNIL, nil] }
|
65
72
|
:VALUE /#{TRUE}/ { [:tBOOLEAN, true] }
|
66
73
|
:VALUE /#{FALSE}/ { [:tBOOLEAN, false] }
|
@@ -76,8 +83,8 @@ rules
|
|
76
83
|
:DYNAMIC_ATTRIBUTE /#{CLOSE_DYNAMIC_ATTRIBUTE}/ { @state = :VALUE; [:tCLOSE_DYNAMIC_ATTRIBUTE, text] }
|
77
84
|
:DYNAMIC_ATTRIBUTE /#{IDENTIFIER}/ { [:tDYNAMIC_ATTRIBUTE, text] }
|
78
85
|
:ARRAY_VALUE /\s+/
|
79
|
-
:ARRAY_VALUE /,/ { [:tCOMMA, text] }
|
80
86
|
:ARRAY_VALUE /#{CLOSE_ARRAY}/ { @state = :VALUE; [:tCLOSE_ARRAY, text] }
|
87
|
+
:ARRAY_VALUE /#{NIL}\?/ { [:tIDENTIFIER_VALUE, text] }
|
81
88
|
:ARRAY_VALUE /#{NIL}/ { [:tNIL, nil] }
|
82
89
|
:ARRAY_VALUE /#{TRUE}/ { [:tBOOLEAN, true] }
|
83
90
|
:ARRAY_VALUE /#{FALSE}/ { [:tBOOLEAN, false] }
|
@@ -20,6 +20,8 @@ class Synvert::Core::NodeQuery::Lexer
|
|
20
20
|
CLOSE_ARRAY = /\)/
|
21
21
|
OPEN_SELECTOR = /\(/
|
22
22
|
CLOSE_SELECTOR = /\)/
|
23
|
+
OPEN_GOTO_SCOPE = /</
|
24
|
+
CLOSE_GOTO_SCOPE = />/
|
23
25
|
OPEN_DYNAMIC_ATTRIBUTE = /{{/
|
24
26
|
CLOSE_DYNAMIC_ATTRIBUTE = /}}/
|
25
27
|
NODE_TYPE = /\.[a-z]+/
|
@@ -149,12 +151,26 @@ class Synvert::Core::NodeQuery::Lexer
|
|
149
151
|
action { [:tOPEN_SELECTOR, text] }
|
150
152
|
when text = ss.scan(/#{CLOSE_SELECTOR}/) then
|
151
153
|
action { [:tCLOSE_SELECTOR, text] }
|
154
|
+
when text = ss.scan(/#{OPEN_GOTO_SCOPE}/) then
|
155
|
+
action { @state = :GOTO_SCOPE; [:tOPEN_GOTO_SCOPE, text] }
|
152
156
|
when text = ss.scan(/#{OPEN_ATTRIBUTE}/) then
|
153
157
|
action { @nested_count += 1; @state = :KEY; [:tOPEN_ATTRIBUTE, text] }
|
154
158
|
else
|
155
159
|
text = ss.string[ss.pos .. -1]
|
156
160
|
raise ScanError, "can not match (#{state.inspect}) at #{location}: '#{text}'"
|
157
161
|
end
|
162
|
+
when :GOTO_SCOPE then
|
163
|
+
case
|
164
|
+
when ss.skip(/\s+/) then
|
165
|
+
# do nothing
|
166
|
+
when text = ss.scan(/#{IDENTIFIER}/) then
|
167
|
+
action { [:tIDENTIFIER, text] }
|
168
|
+
when text = ss.scan(/#{CLOSE_GOTO_SCOPE}/) then
|
169
|
+
action { @state = nil; [:tCLOSE_GOTO_SCOPE, text] }
|
170
|
+
else
|
171
|
+
text = ss.string[ss.pos .. -1]
|
172
|
+
raise ScanError, "can not match (#{state.inspect}) at #{location}: '#{text}'"
|
173
|
+
end
|
158
174
|
when :KEY then
|
159
175
|
case
|
160
176
|
when ss.skip(/\s+/) then
|
@@ -205,6 +221,8 @@ class Synvert::Core::NodeQuery::Lexer
|
|
205
221
|
action { @state = :ARRAY_VALUE; [:tOPEN_ARRAY, text] }
|
206
222
|
when text = ss.scan(/#{CLOSE_ATTRIBUTE}/) then
|
207
223
|
action { @nested_count -= 1; @state = @nested_count == 0 ? nil : :VALUE; [:tCLOSE_ATTRIBUTE, text] }
|
224
|
+
when text = ss.scan(/#{NIL}\?/) then
|
225
|
+
action { [:tIDENTIFIER_VALUE, text] }
|
208
226
|
when ss.skip(/#{NIL}/) then
|
209
227
|
action { [:tNIL, nil] }
|
210
228
|
when ss.skip(/#{TRUE}/) then
|
@@ -247,10 +265,10 @@ class Synvert::Core::NodeQuery::Lexer
|
|
247
265
|
case
|
248
266
|
when ss.skip(/\s+/) then
|
249
267
|
# do nothing
|
250
|
-
when text = ss.scan(/,/) then
|
251
|
-
action { [:tCOMMA, text] }
|
252
268
|
when text = ss.scan(/#{CLOSE_ARRAY}/) then
|
253
269
|
action { @state = :VALUE; [:tCLOSE_ARRAY, text] }
|
270
|
+
when text = ss.scan(/#{NIL}\?/) then
|
271
|
+
action { [:tIDENTIFIER_VALUE, text] }
|
254
272
|
when ss.skip(/#{NIL}/) then
|
255
273
|
action { [:tNIL, nil] }
|
256
274
|
when ss.skip(/#{TRUE}/) then
|