Class: Toml::Merge::NodeWrapper

Inherits:
Ast::Merge::NodeWrapperBase
  • Object
show all
Defined in:
lib/toml/merge/node_wrapper.rb

Overview

Wraps tree-sitter nodes with comment associations, line information, and signatures. This provides a unified interface for working with TOML AST nodes during merging.

Inherits common functionality from Ast::Merge::NodeWrapperBase:

  • Source context (lines, source, comments)
  • Line info extraction
  • Basic methods: #type, #text, #signature

Adds TOML-specific functionality:

  • Backend awareness for Citrus/tree-sitter normalization
  • Type predicates using NodeTypeNormalizer
  • Structural normalization for Citrus backend (pairs as siblings)

Examples:

Basic usage

parser = TreeHaver::Parser.new
parser.language = TreeHaver::Language.toml
tree = parser.parse(source)
wrapper = NodeWrapper.new(tree.root_node, lines: source.lines, source: source)
wrapper.signature # => [:table, "section"]

See Also:

  • Ast::Merge::NodeWrapperBase

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node, lines:, source: nil, leading_comments: nil, inline_comment: nil, **options) ⇒ NodeWrapper

Returns a new instance of NodeWrapper.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/toml/merge/node_wrapper.rb', line 54

def initialize(node, lines:, source: nil, leading_comments: nil, inline_comment: nil, **options)
  @explicit_leading_comments = !leading_comments.nil?
  @explicit_inline_comment = !inline_comment.nil?

  super(
    node,
    lines: lines,
    source: source,
    leading_comments: leading_comments || [],
    inline_comment: inline_comment,
    **options,
  )

  normalize_toml_line_range!
  hydrate_comment_associations!
end

Instance Attribute Details

#backendSymbol (readonly)

Returns The backend used for parsing.

Returns:

  • (Symbol)

    The backend used for parsing



72
73
74
# File 'lib/toml/merge/node_wrapper.rb', line 72

def backend
  @backend
end

#document_rootTreeHaver::Node? (readonly)

Returns The document root node for sibling lookups.

Returns:

  • (TreeHaver::Node, nil)

    The document root node for sibling lookups



75
76
77
# File 'lib/toml/merge/node_wrapper.rb', line 75

def document_root
  @document_root
end

Class Method Details

.wrap(node, lines, source: nil, leading_comments: nil, inline_comment: nil, backend: :tree_sitter, comment_entries: nil, comment_tracker: nil) ⇒ NodeWrapper?

Wrap a tree-sitter node, returning nil for nil input.

Parameters:

  • node (TreeHaver::Node, nil)

    tree-sitter node to wrap

  • lines (Array<String>)

    Source lines for content extraction

  • source (String, nil) (defaults to: nil)

    Original source string

  • leading_comments (Array<Hash>) (defaults to: nil)

    Comments before this node

  • inline_comment (Hash, nil) (defaults to: nil)

    Inline comment on the node's line

  • backend (Symbol) (defaults to: :tree_sitter)

    The backend used for parsing (:tree_sitter or :citrus)

Returns:

  • (NodeWrapper, nil)

    Wrapped node or nil if node is nil



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/toml/merge/node_wrapper.rb', line 37

def wrap(node, lines, source: nil, leading_comments: nil, inline_comment: nil, backend: :tree_sitter,
         comment_entries: nil, comment_tracker: nil)
  return if node.nil?

  new(
    node,
    lines: lines,
    source: source,
    leading_comments: leading_comments,
    inline_comment: inline_comment,
    backend: backend,
    comment_entries: comment_entries,
    comment_tracker: comment_tracker
  )
end

Instance Method Details

#array?Boolean

Check if this is a TOML array

Returns:

  • (Boolean)


146
147
148
# File 'lib/toml/merge/node_wrapper.rb', line 146

def array?
  canonical_type == :array
end

#array_of_tables?Boolean

Check if this is a TOML array of tables Uses NodeTypeNormalizer for backend-agnostic type checking.

Returns:

  • (Boolean)


134
135
136
# File 'lib/toml/merge/node_wrapper.rb', line 134

def array_of_tables?
  canonical_type == :array_of_tables
end

#boolean?Boolean

Check if this is a TOML boolean

Returns:

  • (Boolean)


170
171
172
# File 'lib/toml/merge/node_wrapper.rb', line 170

def boolean?
  canonical_type == :boolean
end

#canonical_typeSymbol

Get the canonical (normalized) type for this node

Returns:

  • (Symbol)


111
112
113
114
115
# File 'lib/toml/merge/node_wrapper.rb', line 111

def canonical_type
  return parslet_element_canonical_type if parslet_element_node?

  NodeTypeNormalizer.canonical_type(@node.type, @backend)
end

#closing_lineString?

Get the closing line for a container node For tables, this is the last line of content before the next table or EOF

Returns:

  • (String, nil)


369
370
371
372
373
# File 'lib/toml/merge/node_wrapper.rb', line 369

def closing_line
  return unless container? && @end_line

  @lines[@end_line - 1]
end

#comment?Boolean

Check if this is a comment

Returns:

  • (Boolean)


182
183
184
# File 'lib/toml/merge/node_wrapper.rb', line 182

def comment?
  canonical_type == :comment
end

#container?Boolean

Check if this node is a container (has mergeable children)

Returns:

  • (Boolean)


353
354
355
# File 'lib/toml/merge/node_wrapper.rb', line 353

def container?
  table? || array_of_tables? || inline_table? || array? || document?
end

#contentString

Get the content for this node from source lines.

Handles structural differences between backends:

  • Tree-sitter: table nodes include pairs, so start_line..end_line covers everything
  • Citrus: table nodes only include header, so we extend to include associated pairs

Returns:

  • (String)


382
383
384
385
386
387
388
389
390
# File 'lib/toml/merge/node_wrapper.rb', line 382

def content
  return '' unless @start_line

  # For tables with Citrus backend, extend end_line to include pairs
  effective_end = effective_end_line
  return '' unless effective_end

  (@start_line..effective_end).map { |ln| @lines[ln - 1] }.compact.join("\n")
end

#datetime?Boolean

Check if this is a datetime

Returns:

  • (Boolean)


188
189
190
# File 'lib/toml/merge/node_wrapper.rb', line 188

def datetime?
  canonical_type == :datetime
end

#document?Boolean

Check if this is the document root

Returns:

  • (Boolean)


194
195
196
# File 'lib/toml/merge/node_wrapper.rb', line 194

def document?
  canonical_type == :document
end

#effective_end_lineInteger?

Get the effective end line for this node, accounting for Citrus backend. For Citrus tables, this extends to the line before the next table.

Returns:

  • (Integer, nil)


395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/toml/merge/node_wrapper.rb', line 395

def effective_end_line
  return @end_line if !(table? || array_of_tables?) || @document_root.nil?

  # Check if we have pairs as children (tree-sitter structure). Some
  # backends expose table node spans that run through the next table
  # boundary, so use the semantic child range when it is available.
  child_pairs = collect_child_pairs
  return child_pairs.map(&:end_line).compact.max || @start_line if child_pairs.any?

  # Citrus structure: find the last pair that belongs to us
  sibling_pairs = collect_sibling_pairs_for_table
  return @end_line if sibling_pairs.empty?

  # Return the end line of the last pair
  sibling_pairs.map(&:end_line).compact.max || @end_line
end

#elementsArray<NodeWrapper>

Get array elements if this is an array

Handles structural differences between backends:

  • Tree-sitter: values are direct children of array node
  • Citrus: values are nested inside array_elements container

Returns:



312
313
314
315
316
317
318
# File 'lib/toml/merge/node_wrapper.rb', line 312

def elements
  return [] unless array?

  result = []
  collect_array_elements(@node, result)
  result
end

#float?Boolean

Check if this is a TOML float

Returns:

  • (Boolean)


164
165
166
# File 'lib/toml/merge/node_wrapper.rb', line 164

def float?
  canonical_type == :float
end

#inline_table?Boolean

Check if this is a TOML inline table

Returns:

  • (Boolean)


140
141
142
# File 'lib/toml/merge/node_wrapper.rb', line 140

def inline_table?
  canonical_type == :inline_table
end

#integer?Boolean

Check if this is a TOML integer

Returns:

  • (Boolean)


158
159
160
# File 'lib/toml/merge/node_wrapper.rb', line 158

def integer?
  canonical_type == :integer
end

#key_nameString?

Get the key name if this is a pair node

Returns:

  • (String, nil)


216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/toml/merge/node_wrapper.rb', line 216

def key_name
  return unless pair?

  # In TOML, pair has key children (bare_key, quoted_key, or dotted_key)
  @node.each do |child|
    child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
    next unless NodeTypeNormalizer.key_type?(child_canonical)

    key_text = node_text(child)
    # Remove surrounding quotes if present, and strip whitespace
    # (Citrus backend includes trailing space in key nodes)
    return key_text&.gsub(/\A["']|["']\z/, '')&.strip
  end
  nil
end

#mergeable_childrenArray<NodeWrapper>

Get mergeable children - the semantically meaningful children for tree merging For tables, returns pairs. For arrays, returns elements. For other node types, returns empty array (leaf nodes).

Returns:



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/toml/merge/node_wrapper.rb', line 324

def mergeable_children
  case canonical_type
  when :table, :inline_table, :array_of_tables
    pairs
  when :array
    elements
  when :document
    # Return top-level pairs and tables
    result = []
    @node.each do |child|
      child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
      next if child_canonical == :comment

      result << NodeWrapper.new(
        child,
        lines: @lines,
        source: @source,
        backend: @backend,
        document_root: @document_root
      )
    end
    result
  else
    []
  end
end

#node_text(ts_node) ⇒ Object



87
88
89
90
91
92
93
94
# File 'lib/toml/merge/node_wrapper.rb', line 87

def node_text(ts_node)
  return '' unless ts_node.respond_to?(:start_byte) && ts_node.respond_to?(:end_byte)

  length = ts_node.end_byte - ts_node.start_byte
  return @source[ts_node.start_byte, length].to_s if @backend == :citrus

  super
end

#normalize_toml_line_range!Object



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/toml/merge/node_wrapper.rb', line 96

def normalize_toml_line_range!
  return unless pair?
  return unless @start_line && @end_line && @end_line > @start_line

  range_text = if @backend == :citrus
                 node_text(citrus_pair_value_node || @node).rstrip
               else
                 node_text(@node).sub(/\n\z/, '')
               end
  line_count = [range_text.lines.count, 1].max
  @end_line = @start_line + line_count - 1
end

#opening_lineString?

Get the opening line for a table (the line with [table_name])

Returns:

  • (String, nil)


359
360
361
362
363
364
# File 'lib/toml/merge/node_wrapper.rb', line 359

def opening_line
  return unless @start_line
  return unless table? || array_of_tables?

  @lines[@start_line - 1]
end

#pair?Boolean

Check if this is a key-value pair

Returns:

  • (Boolean)


176
177
178
# File 'lib/toml/merge/node_wrapper.rb', line 176

def pair?
  canonical_type == :pair
end

#pairsArray<NodeWrapper>

Get key-value pairs from a table or inline_table.

Handles structural differences between backends:

  • Tree-sitter: pairs are children of the table node
  • Citrus: pairs are siblings at document level (table only contains header)

For Citrus backend, when no pair children are found, we look for sibling pairs in the document that belong to this table (pairs after this table's header but before the next table).

Returns:



291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/toml/merge/node_wrapper.rb', line 291

def pairs
  return [] unless table? || inline_table? || document? || array_of_tables?

  # First, try to find pairs as direct children (tree-sitter structure)
  result = collect_child_pairs
  return result if result.any?

  # For Citrus backend: pairs are siblings, not children
  # Look for pairs in document that belong to this table
  return [] if @document_root.nil? || !(table? || array_of_tables?)

  collect_sibling_pairs_for_table
end

#process_additional_options(options) ⇒ Object

Process TOML-specific options (backend, document_root)

Parameters:

  • options (Hash)

    Additional options



79
80
81
82
83
84
85
# File 'lib/toml/merge/node_wrapper.rb', line 79

def process_additional_options(options)
  @backend = options.fetch(:backend, :tree_sitter)
  @document_root = options[:document_root]
  @comment_entries = Array(options[:comment_entries])
  @comment_tracker = options[:comment_tracker]
  @raw_text = options[:raw_text]
end

#string?Boolean

Check if this is a TOML string

Returns:

  • (Boolean)


152
153
154
# File 'lib/toml/merge/node_wrapper.rb', line 152

def string?
  canonical_type == :string
end

#table?Boolean

Check if this is a TOML table (section)

Returns:

  • (Boolean)


127
128
129
# File 'lib/toml/merge/node_wrapper.rb', line 127

def table?
  canonical_type == :table
end

#table_nameString?

Get the table name (header) if this is a table

Returns:

  • (String, nil)


200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/toml/merge/node_wrapper.rb', line 200

def table_name
  return unless table? || array_of_tables?

  # Find the dotted_key or bare_key child that represents the table name
  table_name_container.each do |child|
    child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
    if NodeTypeNormalizer.key_type?(child_canonical)
      # Strip whitespace (Citrus backend includes trailing space in key nodes)
      return node_text(child)&.strip
    end
  end
  parslet_element_table_name
end

#textObject



274
275
276
277
278
# File 'lib/toml/merge/node_wrapper.rb', line 274

def text
  return @raw_text if @raw_text

  super
end

#type?(type_name) ⇒ Boolean

Check if this node has a specific type (checks both raw and canonical)

Parameters:

  • type_name (Symbol, String)

    Type to check

Returns:

  • (Boolean)


120
121
122
123
# File 'lib/toml/merge/node_wrapper.rb', line 120

def type?(type_name)
  type_sym = type_name.to_sym
  @node.type.to_sym == type_sym || canonical_type == type_sym
end

#value_nodeNodeWrapper?

Get the value node if this is a pair

Returns:



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/toml/merge/node_wrapper.rb', line 234

def value_node
  return unless pair?

  @node.each do |child|
    child_canonical = NodeTypeNormalizer.canonical_type(child.type, @backend)
    # Skip keys, equals sign, whitespace, and unknown (Citrus uses these for delimiters)
    next if NodeTypeNormalizer.key_type?(child_canonical)
    next if %i[equals whitespace unknown space].include?(child_canonical)

    if @backend == :parslet && child_canonical == :value
      scalar_child = each_child(child).find do |grandchild|
        grandchild_canonical = NodeTypeNormalizer.canonical_type(grandchild.type, @backend)
        !%i[whitespace unknown space].include?(grandchild_canonical)
      end

      if scalar_child
        return NodeWrapper.new(
          scalar_child,
          lines: @lines,
          source: @source,
          backend: @backend,
          document_root: @document_root,
          comment_entries: @comment_entries,
          comment_tracker: comment_tracker,
          raw_text: raw_pair_value_text
        )
      end
    end

    return NodeWrapper.new(
      child,
      lines: @lines,
      source: @source,
      backend: @backend,
      document_root: @document_root
    )
  end
  nil
end