toml-merge 1.0.0 → 2.0.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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +181 -1
- data/LICENSE.txt +1 -1
- data/README.md +294 -144
- data/lib/toml/merge/conflict_resolver.rb +60 -107
- data/lib/toml/merge/emitter.rb +120 -0
- data/lib/toml/merge/file_analysis.rb +122 -39
- data/lib/toml/merge/merge_result.rb +10 -4
- data/lib/toml/merge/node_type_normalizer.rb +256 -0
- data/lib/toml/merge/node_wrapper.rb +342 -177
- data/lib/toml/merge/smart_merger.rb +42 -4
- data/lib/toml/merge/table_match_refiner.rb +4 -2
- data/lib/toml/merge/version.rb +1 -1
- data/lib/toml/merge.rb +8 -33
- data.tar.gz.sig +0 -0
- metadata +22 -22
- metadata.gz.sig +0 -0
|
@@ -5,13 +5,25 @@ module Toml
|
|
|
5
5
|
# Wraps tree-sitter nodes with comment associations, line information, and signatures.
|
|
6
6
|
# This provides a unified interface for working with TOML AST nodes during merging.
|
|
7
7
|
#
|
|
8
|
+
# Inherits common functionality from Ast::Merge::NodeWrapperBase:
|
|
9
|
+
# - Source context (lines, source, comments)
|
|
10
|
+
# - Line info extraction
|
|
11
|
+
# - Basic methods: #type, #text, #signature
|
|
12
|
+
#
|
|
13
|
+
# Adds TOML-specific functionality:
|
|
14
|
+
# - Backend awareness for Citrus/tree-sitter normalization
|
|
15
|
+
# - Type predicates using NodeTypeNormalizer
|
|
16
|
+
# - Structural normalization for Citrus backend (pairs as siblings)
|
|
17
|
+
#
|
|
8
18
|
# @example Basic usage
|
|
9
19
|
# parser = TreeHaver::Parser.new
|
|
10
|
-
# parser.language = TreeHaver::Language.
|
|
11
|
-
# tree = parser.
|
|
20
|
+
# parser.language = TreeHaver::Language.toml
|
|
21
|
+
# tree = parser.parse(source)
|
|
12
22
|
# wrapper = NodeWrapper.new(tree.root_node, lines: source.lines, source: source)
|
|
13
23
|
# wrapper.signature # => [:table, "section"]
|
|
14
|
-
|
|
24
|
+
#
|
|
25
|
+
# @see Ast::Merge::NodeWrapperBase
|
|
26
|
+
class NodeWrapper < Ast::Merge::NodeWrapperBase
|
|
15
27
|
class << self
|
|
16
28
|
# Wrap a tree-sitter node, returning nil for nil input.
|
|
17
29
|
#
|
|
@@ -20,141 +32,120 @@ module Toml
|
|
|
20
32
|
# @param source [String, nil] Original source string
|
|
21
33
|
# @param leading_comments [Array<Hash>] Comments before this node
|
|
22
34
|
# @param inline_comment [Hash, nil] Inline comment on the node's line
|
|
35
|
+
# @param backend [Symbol] The backend used for parsing (:tree_sitter or :citrus)
|
|
23
36
|
# @return [NodeWrapper, nil] Wrapped node or nil if node is nil
|
|
24
|
-
def wrap(node, lines, source: nil, leading_comments: [], inline_comment: nil)
|
|
37
|
+
def wrap(node, lines, source: nil, leading_comments: [], inline_comment: nil, backend: :tree_sitter)
|
|
25
38
|
return if node.nil?
|
|
26
39
|
|
|
27
|
-
new(
|
|
40
|
+
new(
|
|
41
|
+
node,
|
|
42
|
+
lines: lines,
|
|
43
|
+
source: source,
|
|
44
|
+
leading_comments: leading_comments,
|
|
45
|
+
inline_comment: inline_comment,
|
|
46
|
+
backend: backend,
|
|
47
|
+
)
|
|
28
48
|
end
|
|
29
49
|
end
|
|
30
50
|
|
|
31
|
-
# @return [
|
|
32
|
-
attr_reader :
|
|
33
|
-
|
|
34
|
-
# @return [Array<Hash>] Leading comments associated with this node
|
|
35
|
-
attr_reader :leading_comments
|
|
36
|
-
|
|
37
|
-
# @return [String] The original source string
|
|
38
|
-
attr_reader :source
|
|
39
|
-
|
|
40
|
-
# @return [Hash, nil] Inline/trailing comment on the same line
|
|
41
|
-
attr_reader :inline_comment
|
|
42
|
-
|
|
43
|
-
# @return [Integer] Start line (1-based)
|
|
44
|
-
attr_reader :start_line
|
|
45
|
-
|
|
46
|
-
# @return [Integer] End line (1-based)
|
|
47
|
-
attr_reader :end_line
|
|
51
|
+
# @return [Symbol] The backend used for parsing
|
|
52
|
+
attr_reader :backend
|
|
48
53
|
|
|
49
|
-
# @return [
|
|
50
|
-
attr_reader :
|
|
54
|
+
# @return [TreeHaver::Node, nil] The document root node for sibling lookups
|
|
55
|
+
attr_reader :document_root
|
|
51
56
|
|
|
52
|
-
#
|
|
53
|
-
# @param
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def initialize(node, lines:, source: nil, leading_comments: [], inline_comment: nil)
|
|
58
|
-
@node = node
|
|
59
|
-
@lines = lines
|
|
60
|
-
@source = source || lines.join("\n")
|
|
61
|
-
@leading_comments = leading_comments
|
|
62
|
-
@inline_comment = inline_comment
|
|
63
|
-
|
|
64
|
-
# Extract line information from the tree-sitter node (0-indexed to 1-indexed)
|
|
65
|
-
@start_line = node.start_point.row + 1 if node.respond_to?(:start_point)
|
|
66
|
-
@end_line = node.end_point.row + 1 if node.respond_to?(:end_point)
|
|
67
|
-
|
|
68
|
-
# Handle edge case where end_line might be before start_line
|
|
69
|
-
@end_line = @start_line if @start_line && @end_line && @end_line < @start_line
|
|
57
|
+
# Process TOML-specific options (backend, document_root)
|
|
58
|
+
# @param options [Hash] Additional options
|
|
59
|
+
def process_additional_options(options)
|
|
60
|
+
@backend = options.fetch(:backend, :tree_sitter)
|
|
61
|
+
@document_root = options[:document_root]
|
|
70
62
|
end
|
|
71
63
|
|
|
72
|
-
#
|
|
73
|
-
# Signatures are used to identify corresponding nodes between template and destination.
|
|
74
|
-
#
|
|
75
|
-
# @return [Array, nil] Signature array or nil if not signaturable
|
|
76
|
-
def signature
|
|
77
|
-
compute_signature(@node)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Get the node type as a symbol
|
|
64
|
+
# Get the canonical (normalized) type for this node
|
|
81
65
|
# @return [Symbol]
|
|
82
|
-
def
|
|
83
|
-
@node.type
|
|
66
|
+
def canonical_type
|
|
67
|
+
NodeTypeNormalizer.canonical_type(@node.type, @backend)
|
|
84
68
|
end
|
|
85
69
|
|
|
86
|
-
# Check if this node has a specific type
|
|
70
|
+
# Check if this node has a specific type (checks both raw and canonical)
|
|
87
71
|
# @param type_name [Symbol, String] Type to check
|
|
88
72
|
# @return [Boolean]
|
|
89
73
|
def type?(type_name)
|
|
90
|
-
|
|
74
|
+
type_sym = type_name.to_sym
|
|
75
|
+
@node.type.to_sym == type_sym || canonical_type == type_sym
|
|
91
76
|
end
|
|
92
77
|
|
|
93
78
|
# Check if this is a TOML table (section)
|
|
94
79
|
# @return [Boolean]
|
|
95
80
|
def table?
|
|
96
|
-
|
|
81
|
+
canonical_type == :table
|
|
97
82
|
end
|
|
98
83
|
|
|
99
84
|
# Check if this is a TOML array of tables
|
|
85
|
+
# Uses NodeTypeNormalizer for backend-agnostic type checking.
|
|
100
86
|
# @return [Boolean]
|
|
101
87
|
def array_of_tables?
|
|
102
|
-
|
|
103
|
-
type_str == "array_of_tables" || type_str == "table_array_element"
|
|
88
|
+
canonical_type == :array_of_tables
|
|
104
89
|
end
|
|
105
90
|
|
|
106
91
|
# Check if this is a TOML inline table
|
|
107
92
|
# @return [Boolean]
|
|
108
93
|
def inline_table?
|
|
109
|
-
|
|
94
|
+
canonical_type == :inline_table
|
|
110
95
|
end
|
|
111
96
|
|
|
112
97
|
# Check if this is a TOML array
|
|
113
98
|
# @return [Boolean]
|
|
114
99
|
def array?
|
|
115
|
-
|
|
100
|
+
canonical_type == :array
|
|
116
101
|
end
|
|
117
102
|
|
|
118
103
|
# Check if this is a TOML string
|
|
119
104
|
# @return [Boolean]
|
|
120
105
|
def string?
|
|
121
|
-
|
|
106
|
+
canonical_type == :string
|
|
122
107
|
end
|
|
123
108
|
|
|
124
109
|
# Check if this is a TOML integer
|
|
125
110
|
# @return [Boolean]
|
|
126
111
|
def integer?
|
|
127
|
-
|
|
112
|
+
canonical_type == :integer
|
|
128
113
|
end
|
|
129
114
|
|
|
130
115
|
# Check if this is a TOML float
|
|
131
116
|
# @return [Boolean]
|
|
132
117
|
def float?
|
|
133
|
-
|
|
118
|
+
canonical_type == :float
|
|
134
119
|
end
|
|
135
120
|
|
|
136
121
|
# Check if this is a TOML boolean
|
|
137
122
|
# @return [Boolean]
|
|
138
123
|
def boolean?
|
|
139
|
-
|
|
124
|
+
canonical_type == :boolean
|
|
140
125
|
end
|
|
141
126
|
|
|
142
127
|
# Check if this is a key-value pair
|
|
143
128
|
# @return [Boolean]
|
|
144
129
|
def pair?
|
|
145
|
-
|
|
130
|
+
canonical_type == :pair
|
|
146
131
|
end
|
|
147
132
|
|
|
148
133
|
# Check if this is a comment
|
|
149
134
|
# @return [Boolean]
|
|
150
135
|
def comment?
|
|
151
|
-
|
|
136
|
+
canonical_type == :comment
|
|
152
137
|
end
|
|
153
138
|
|
|
154
139
|
# Check if this is a datetime
|
|
155
140
|
# @return [Boolean]
|
|
156
141
|
def datetime?
|
|
157
|
-
|
|
142
|
+
canonical_type == :datetime
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Check if this is the document root
|
|
146
|
+
# @return [Boolean]
|
|
147
|
+
def document?
|
|
148
|
+
canonical_type == :document
|
|
158
149
|
end
|
|
159
150
|
|
|
160
151
|
# Get the table name (header) if this is a table
|
|
@@ -164,9 +155,10 @@ module Toml
|
|
|
164
155
|
|
|
165
156
|
# Find the dotted_key or bare_key child that represents the table name
|
|
166
157
|
@node.each do |child|
|
|
167
|
-
|
|
168
|
-
if
|
|
169
|
-
|
|
158
|
+
child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
|
|
159
|
+
if NodeTypeNormalizer.key_type?(child_canonical)
|
|
160
|
+
# Strip whitespace (Citrus backend includes trailing space in key nodes)
|
|
161
|
+
return node_text(child)&.strip
|
|
170
162
|
end
|
|
171
163
|
end
|
|
172
164
|
nil
|
|
@@ -177,13 +169,14 @@ module Toml
|
|
|
177
169
|
def key_name
|
|
178
170
|
return unless pair?
|
|
179
171
|
|
|
180
|
-
# In TOML
|
|
172
|
+
# In TOML, pair has key children (bare_key, quoted_key, or dotted_key)
|
|
181
173
|
@node.each do |child|
|
|
182
|
-
|
|
183
|
-
if
|
|
174
|
+
child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
|
|
175
|
+
if NodeTypeNormalizer.key_type?(child_canonical)
|
|
184
176
|
key_text = node_text(child)
|
|
185
|
-
# Remove surrounding quotes if present
|
|
186
|
-
|
|
177
|
+
# Remove surrounding quotes if present, and strip whitespace
|
|
178
|
+
# (Citrus backend includes trailing space in key nodes)
|
|
179
|
+
return key_text&.gsub(/\A["']|["']\z/, "")&.strip
|
|
187
180
|
end
|
|
188
181
|
end
|
|
189
182
|
nil
|
|
@@ -195,61 +188,59 @@ module Toml
|
|
|
195
188
|
return unless pair?
|
|
196
189
|
|
|
197
190
|
@node.each do |child|
|
|
198
|
-
|
|
199
|
-
# Skip keys,
|
|
200
|
-
next if
|
|
201
|
-
|
|
202
|
-
|
|
191
|
+
child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
|
|
192
|
+
# Skip keys, equals sign, whitespace, and unknown (Citrus uses these for delimiters)
|
|
193
|
+
next if NodeTypeNormalizer.key_type?(child_canonical)
|
|
194
|
+
next if %i[equals whitespace unknown space].include?(child_canonical)
|
|
195
|
+
|
|
196
|
+
return NodeWrapper.new(
|
|
197
|
+
child,
|
|
198
|
+
lines: @lines,
|
|
199
|
+
source: @source,
|
|
200
|
+
backend: @backend,
|
|
201
|
+
document_root: @document_root,
|
|
202
|
+
)
|
|
203
203
|
end
|
|
204
204
|
nil
|
|
205
205
|
end
|
|
206
206
|
|
|
207
|
-
# Get key-value pairs from a table or inline_table
|
|
207
|
+
# Get key-value pairs from a table or inline_table.
|
|
208
|
+
#
|
|
209
|
+
# Handles structural differences between backends:
|
|
210
|
+
# - Tree-sitter: pairs are children of the table node
|
|
211
|
+
# - Citrus: pairs are siblings at document level (table only contains header)
|
|
212
|
+
#
|
|
213
|
+
# For Citrus backend, when no pair children are found, we look for sibling
|
|
214
|
+
# pairs in the document that belong to this table (pairs after this table's
|
|
215
|
+
# header but before the next table).
|
|
216
|
+
#
|
|
208
217
|
# @return [Array<NodeWrapper>]
|
|
209
218
|
def pairs
|
|
210
|
-
return [] unless table? || inline_table? || document?
|
|
219
|
+
return [] unless table? || inline_table? || document? || array_of_tables?
|
|
211
220
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
# First, try to find pairs as direct children (tree-sitter structure)
|
|
222
|
+
result = collect_child_pairs
|
|
223
|
+
return result if result.any?
|
|
215
224
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
end
|
|
225
|
+
# For Citrus backend: pairs are siblings, not children
|
|
226
|
+
# Look for pairs in document that belong to this table
|
|
227
|
+
return [] if @document_root.nil? || !(table? || array_of_tables?)
|
|
220
228
|
|
|
221
|
-
|
|
222
|
-
# @return [Boolean]
|
|
223
|
-
def document?
|
|
224
|
-
@node.type.to_s == "document"
|
|
229
|
+
collect_sibling_pairs_for_table
|
|
225
230
|
end
|
|
226
231
|
|
|
227
232
|
# Get array elements if this is an array
|
|
233
|
+
#
|
|
234
|
+
# Handles structural differences between backends:
|
|
235
|
+
# - Tree-sitter: values are direct children of array node
|
|
236
|
+
# - Citrus: values are nested inside array_elements container
|
|
237
|
+
#
|
|
228
238
|
# @return [Array<NodeWrapper>]
|
|
229
239
|
def elements
|
|
230
240
|
return [] unless array?
|
|
231
241
|
|
|
232
242
|
result = []
|
|
233
|
-
@node
|
|
234
|
-
child_type = child.type.to_s
|
|
235
|
-
# Skip punctuation and comments
|
|
236
|
-
next if child_type == "comment"
|
|
237
|
-
next if %w[, \[ \]].include?(child_type)
|
|
238
|
-
|
|
239
|
-
result << NodeWrapper.new(child, lines: @lines, source: @source)
|
|
240
|
-
end
|
|
241
|
-
result
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# Get children wrapped as NodeWrappers
|
|
245
|
-
# @return [Array<NodeWrapper>]
|
|
246
|
-
def children
|
|
247
|
-
return [] unless @node.respond_to?(:each)
|
|
248
|
-
|
|
249
|
-
result = []
|
|
250
|
-
@node.each do |child|
|
|
251
|
-
result << NodeWrapper.new(child, lines: @lines, source: @source)
|
|
252
|
-
end
|
|
243
|
+
collect_array_elements(@node, result)
|
|
253
244
|
result
|
|
254
245
|
end
|
|
255
246
|
|
|
@@ -258,8 +249,8 @@ module Toml
|
|
|
258
249
|
# For other node types, returns empty array (leaf nodes).
|
|
259
250
|
# @return [Array<NodeWrapper>]
|
|
260
251
|
def mergeable_children
|
|
261
|
-
case
|
|
262
|
-
when :table, :inline_table
|
|
252
|
+
case canonical_type
|
|
253
|
+
when :table, :inline_table, :array_of_tables
|
|
263
254
|
pairs
|
|
264
255
|
when :array
|
|
265
256
|
elements
|
|
@@ -267,10 +258,16 @@ module Toml
|
|
|
267
258
|
# Return top-level pairs and tables
|
|
268
259
|
result = []
|
|
269
260
|
@node.each do |child|
|
|
270
|
-
|
|
271
|
-
next if
|
|
272
|
-
|
|
273
|
-
result << NodeWrapper.new(
|
|
261
|
+
child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
|
|
262
|
+
next if child_canonical == :comment
|
|
263
|
+
|
|
264
|
+
result << NodeWrapper.new(
|
|
265
|
+
child,
|
|
266
|
+
lines: @lines,
|
|
267
|
+
source: @source,
|
|
268
|
+
backend: @backend,
|
|
269
|
+
document_root: @document_root,
|
|
270
|
+
)
|
|
274
271
|
end
|
|
275
272
|
result
|
|
276
273
|
else
|
|
@@ -284,12 +281,6 @@ module Toml
|
|
|
284
281
|
table? || array_of_tables? || inline_table? || array? || document?
|
|
285
282
|
end
|
|
286
283
|
|
|
287
|
-
# Check if this node is a leaf (no mergeable children)
|
|
288
|
-
# @return [Boolean]
|
|
289
|
-
def leaf?
|
|
290
|
-
!container?
|
|
291
|
-
end
|
|
292
|
-
|
|
293
284
|
# Get the opening line for a table (the line with [table_name])
|
|
294
285
|
# @return [String, nil]
|
|
295
286
|
def opening_line
|
|
@@ -308,109 +299,283 @@ module Toml
|
|
|
308
299
|
@lines[@end_line - 1]
|
|
309
300
|
end
|
|
310
301
|
|
|
311
|
-
# Get the
|
|
302
|
+
# Get the content for this node from source lines.
|
|
303
|
+
#
|
|
304
|
+
# Handles structural differences between backends:
|
|
305
|
+
# - Tree-sitter: table nodes include pairs, so start_line..end_line covers everything
|
|
306
|
+
# - Citrus: table nodes only include header, so we extend to include associated pairs
|
|
307
|
+
#
|
|
312
308
|
# @return [String]
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
end
|
|
309
|
+
def content
|
|
310
|
+
return "" unless @start_line
|
|
316
311
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def node_text(ts_node)
|
|
321
|
-
return "" unless ts_node.respond_to?(:start_byte) && ts_node.respond_to?(:end_byte)
|
|
312
|
+
# For tables with Citrus backend, extend end_line to include pairs
|
|
313
|
+
effective_end = effective_end_line
|
|
314
|
+
return "" unless effective_end
|
|
322
315
|
|
|
323
|
-
@
|
|
316
|
+
(@start_line..effective_end).map { |ln| @lines[ln - 1] }.compact.join("\n")
|
|
324
317
|
end
|
|
325
318
|
|
|
326
|
-
# Get the
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
319
|
+
# Get the effective end line for this node, accounting for Citrus backend.
|
|
320
|
+
# For Citrus tables, this extends to the line before the next table.
|
|
321
|
+
# @return [Integer, nil]
|
|
322
|
+
def effective_end_line
|
|
323
|
+
return @end_line if !(table? || array_of_tables?) || @document_root.nil?
|
|
330
324
|
|
|
331
|
-
|
|
332
|
-
|
|
325
|
+
# Check if we have pairs as children (tree-sitter structure)
|
|
326
|
+
child_pairs = collect_child_pairs
|
|
327
|
+
return @end_line if child_pairs.any?
|
|
333
328
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
329
|
+
# Citrus structure: find the last pair that belongs to us
|
|
330
|
+
sibling_pairs = collect_sibling_pairs_for_table
|
|
331
|
+
return @end_line if sibling_pairs.empty?
|
|
332
|
+
|
|
333
|
+
# Return the end line of the last pair
|
|
334
|
+
sibling_pairs.map(&:end_line).compact.max || @end_line
|
|
338
335
|
end
|
|
339
336
|
|
|
340
|
-
|
|
337
|
+
protected
|
|
338
|
+
|
|
339
|
+
# Override wrap_child to use Toml::Merge::NodeWrapper with proper options
|
|
340
|
+
def wrap_child(child)
|
|
341
|
+
NodeWrapper.new(
|
|
342
|
+
child,
|
|
343
|
+
lines: @lines,
|
|
344
|
+
source: @source,
|
|
345
|
+
backend: @backend,
|
|
346
|
+
document_root: @document_root,
|
|
347
|
+
)
|
|
348
|
+
end
|
|
341
349
|
|
|
342
350
|
def compute_signature(node)
|
|
343
|
-
|
|
351
|
+
# Use canonical type for signature generation
|
|
352
|
+
# Pass @backend to ensure correct type mapping for Citrus vs tree-sitter
|
|
353
|
+
canonical = NodeTypeNormalizer.canonical_type(node.type, @backend)
|
|
344
354
|
|
|
345
|
-
case
|
|
346
|
-
when
|
|
355
|
+
case canonical
|
|
356
|
+
when :document
|
|
347
357
|
# Root document
|
|
348
358
|
[:document]
|
|
349
|
-
when
|
|
359
|
+
when :table
|
|
350
360
|
# Tables identified by their header name
|
|
351
361
|
name = table_name
|
|
352
362
|
[:table, name]
|
|
353
|
-
when
|
|
363
|
+
when :array_of_tables
|
|
354
364
|
# Array of tables identified by their header name
|
|
355
365
|
name = table_name
|
|
356
366
|
[:array_of_tables, name]
|
|
357
|
-
when
|
|
367
|
+
when :pair
|
|
358
368
|
# Pairs identified by their key name
|
|
359
369
|
key = key_name
|
|
360
370
|
[:pair, key]
|
|
361
|
-
when
|
|
371
|
+
when :inline_table
|
|
362
372
|
# Inline tables identified by their keys
|
|
363
373
|
keys = extract_inline_table_keys(node)
|
|
364
374
|
[:inline_table, keys.sort]
|
|
365
|
-
when
|
|
375
|
+
when :array
|
|
366
376
|
# Arrays identified by their length
|
|
367
377
|
elements_count = 0
|
|
368
|
-
node.each
|
|
369
|
-
next if %w[comment , \[ \]].include?(c.type.to_s)
|
|
370
|
-
|
|
371
|
-
elements_count += 1
|
|
372
|
-
end
|
|
378
|
+
node.each { |c| elements_count += 1 unless %i[comment comma bracket_open bracket_close].include?(NodeTypeNormalizer.canonical_type(c.type, @backend)) }
|
|
373
379
|
[:array, elements_count]
|
|
374
|
-
when
|
|
380
|
+
when :string
|
|
375
381
|
# Strings identified by their content
|
|
376
382
|
[:string, node_text(node)]
|
|
377
|
-
when
|
|
383
|
+
when :integer
|
|
378
384
|
# Integers identified by their value
|
|
379
385
|
[:integer, node_text(node)]
|
|
380
|
-
when
|
|
386
|
+
when :float
|
|
381
387
|
# Floats identified by their value
|
|
382
388
|
[:float, node_text(node)]
|
|
383
|
-
when
|
|
389
|
+
when :boolean
|
|
384
390
|
# Booleans
|
|
385
391
|
[:boolean, node_text(node)]
|
|
386
|
-
when
|
|
392
|
+
when :datetime
|
|
387
393
|
# Datetime values
|
|
388
394
|
[:datetime, node_text(node)]
|
|
389
|
-
when
|
|
395
|
+
when :comment
|
|
390
396
|
# Comments identified by their content
|
|
391
397
|
[:comment, node_text(node)&.strip]
|
|
392
398
|
else
|
|
393
|
-
# Generic fallback
|
|
399
|
+
# Generic fallback - use canonical type in signature
|
|
394
400
|
content_preview = node_text(node)&.slice(0, 50)&.strip
|
|
395
|
-
[
|
|
401
|
+
[canonical, content_preview]
|
|
396
402
|
end
|
|
397
403
|
end
|
|
398
404
|
|
|
405
|
+
private
|
|
406
|
+
|
|
399
407
|
def extract_inline_table_keys(inline_table_node)
|
|
400
408
|
keys = []
|
|
401
|
-
inline_table_node
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
409
|
+
collect_inline_table_keys_recursive(inline_table_node, keys)
|
|
410
|
+
keys
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Recursively collect keys from inline table, handling both tree-sitter and Citrus structures.
|
|
414
|
+
#
|
|
415
|
+
# Tree-sitter inline_table structure:
|
|
416
|
+
# inline_table -> pair -> bare_key (direct children)
|
|
417
|
+
#
|
|
418
|
+
# Citrus inline_table structure:
|
|
419
|
+
# inline_table -> optional -> keyvalue -> keyvalue -> stripped_key -> key -> bare_key
|
|
420
|
+
# With repeat and unknown nodes containing additional key-values
|
|
421
|
+
#
|
|
422
|
+
def collect_inline_table_keys_recursive(node, keys)
|
|
423
|
+
node.each do |child|
|
|
424
|
+
child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
|
|
425
|
+
child_type_raw = child.type.to_sym
|
|
426
|
+
|
|
427
|
+
# For Citrus: recurse into container nodes that hold pairs/keys
|
|
428
|
+
# For tree-sitter: only recurse into pairs
|
|
429
|
+
if @backend == :citrus
|
|
430
|
+
if %i[optional repeat keyvalue unknown].include?(child_type_raw)
|
|
431
|
+
collect_inline_table_keys_recursive(child, keys)
|
|
432
|
+
next
|
|
433
|
+
end
|
|
434
|
+
elsif child_canonical == :pair
|
|
435
|
+
# Tree-sitter: pairs contain keys directly
|
|
436
|
+
child.each do |pair_child|
|
|
437
|
+
pair_child_canonical = NodeTypeNormalizer.canonical_type(pair_child.type, @backend)
|
|
438
|
+
if NodeTypeNormalizer.key_type?(pair_child_canonical)
|
|
439
|
+
key_text = node_text(pair_child)&.gsub(/\A["']|["']\z/, "")&.strip
|
|
440
|
+
keys << key_text if key_text && !key_text.empty?
|
|
441
|
+
break
|
|
442
|
+
end
|
|
410
443
|
end
|
|
444
|
+
next
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Skip whitespace and punctuation
|
|
448
|
+
next if %i[whitespace space brace_open brace_close comma].include?(child_canonical)
|
|
449
|
+
|
|
450
|
+
# Found a key node - extract the key text (Citrus path)
|
|
451
|
+
if NodeTypeNormalizer.key_type?(child_canonical)
|
|
452
|
+
key_text = node_text(child)&.gsub(/\A["']|["']\z/, "")&.strip
|
|
453
|
+
keys << key_text if key_text && !key_text.empty?
|
|
411
454
|
end
|
|
412
455
|
end
|
|
413
|
-
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Collect pairs that are direct children of this node.
|
|
459
|
+
# This is the standard tree-sitter structure.
|
|
460
|
+
# @return [Array<NodeWrapper>]
|
|
461
|
+
def collect_child_pairs
|
|
462
|
+
result = []
|
|
463
|
+
@node.each do |child|
|
|
464
|
+
child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
|
|
465
|
+
next unless child_canonical == :pair
|
|
466
|
+
|
|
467
|
+
result << NodeWrapper.new(
|
|
468
|
+
child,
|
|
469
|
+
lines: @lines,
|
|
470
|
+
source: @source,
|
|
471
|
+
backend: @backend,
|
|
472
|
+
document_root: @document_root,
|
|
473
|
+
)
|
|
474
|
+
end
|
|
475
|
+
result
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Collect pairs from document siblings that belong to this table.
|
|
479
|
+
# Used for Citrus backend where pairs are siblings, not children.
|
|
480
|
+
#
|
|
481
|
+
# A pair belongs to this table if:
|
|
482
|
+
# - It appears after this table's header line
|
|
483
|
+
# - It appears before the next table's header line (or end of document)
|
|
484
|
+
#
|
|
485
|
+
# @return [Array<NodeWrapper>]
|
|
486
|
+
def collect_sibling_pairs_for_table
|
|
487
|
+
result = []
|
|
488
|
+
my_start = @start_line
|
|
489
|
+
return result unless my_start
|
|
490
|
+
|
|
491
|
+
# Find the next table's start line (to know where our pairs end)
|
|
492
|
+
next_table_start = find_next_table_start_line
|
|
493
|
+
|
|
494
|
+
# Iterate through document children to find pairs in our range
|
|
495
|
+
@document_root.each do |sibling|
|
|
496
|
+
sibling_canonical = NodeTypeNormalizer.canonical_type(sibling.type, @backend)
|
|
497
|
+
next unless sibling_canonical == :pair
|
|
498
|
+
|
|
499
|
+
sibling_start = sibling.respond_to?(:start_point) ? sibling.start_point.row + 1 : nil
|
|
500
|
+
next unless sibling_start
|
|
501
|
+
|
|
502
|
+
# Pair must be after our header
|
|
503
|
+
next if sibling_start <= my_start
|
|
504
|
+
|
|
505
|
+
# Pair must be before the next table (if there is one)
|
|
506
|
+
next if next_table_start && sibling_start >= next_table_start
|
|
507
|
+
|
|
508
|
+
result << NodeWrapper.new(
|
|
509
|
+
sibling,
|
|
510
|
+
lines: @lines,
|
|
511
|
+
source: @source,
|
|
512
|
+
backend: @backend,
|
|
513
|
+
document_root: @document_root,
|
|
514
|
+
)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
result
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Find the start line of the next table in the document.
|
|
521
|
+
# Returns nil if this is the last table.
|
|
522
|
+
# @return [Integer, nil]
|
|
523
|
+
def find_next_table_start_line
|
|
524
|
+
return unless @document_root
|
|
525
|
+
|
|
526
|
+
my_start = @start_line
|
|
527
|
+
next_table_start = nil
|
|
528
|
+
|
|
529
|
+
@document_root.each do |sibling|
|
|
530
|
+
sibling_canonical = NodeTypeNormalizer.canonical_type(sibling.type, @backend)
|
|
531
|
+
next unless NodeTypeNormalizer.table_type?(sibling_canonical)
|
|
532
|
+
|
|
533
|
+
sibling_start = sibling.respond_to?(:start_point) ? sibling.start_point.row + 1 : nil
|
|
534
|
+
next unless sibling_start
|
|
535
|
+
next if sibling_start <= my_start
|
|
536
|
+
|
|
537
|
+
# Found a table after us - track the closest one
|
|
538
|
+
if next_table_start.nil? || sibling_start < next_table_start
|
|
539
|
+
next_table_start = sibling_start
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
next_table_start
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Recursively collect array elements, handling Citrus's nested structure.
|
|
547
|
+
#
|
|
548
|
+
# Citrus array structure:
|
|
549
|
+
# array -> array_elements -> array_elements -> decimal_integer + repeat
|
|
550
|
+
# repeat -> indent -> decimal_integer (for each subsequent element)
|
|
551
|
+
#
|
|
552
|
+
# @param node [Object] Node to collect elements from
|
|
553
|
+
# @param result [Array<NodeWrapper>] Array to append elements to
|
|
554
|
+
def collect_array_elements(node, result)
|
|
555
|
+
node.each do |child|
|
|
556
|
+
child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
|
|
557
|
+
child_type_raw = child.type.to_sym
|
|
558
|
+
|
|
559
|
+
# For Citrus: recurse into container nodes that hold values
|
|
560
|
+
if %i[array_elements repeat indent].include?(child_type_raw)
|
|
561
|
+
collect_array_elements(child, result)
|
|
562
|
+
next
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Skip punctuation, comments, whitespace, and structural nodes
|
|
566
|
+
next if child_canonical == :comment
|
|
567
|
+
next if %i[comma bracket_open bracket_close].include?(child_canonical)
|
|
568
|
+
next if %i[whitespace unknown space array_comments sign].include?(child_canonical)
|
|
569
|
+
|
|
570
|
+
# This is an actual value element (integer, string, boolean, etc.)
|
|
571
|
+
result << NodeWrapper.new(
|
|
572
|
+
child,
|
|
573
|
+
lines: @lines,
|
|
574
|
+
source: @source,
|
|
575
|
+
backend: @backend,
|
|
576
|
+
document_root: @document_root,
|
|
577
|
+
)
|
|
578
|
+
end
|
|
414
579
|
end
|
|
415
580
|
end
|
|
416
581
|
end
|