Class: Ast::Merge::EmitterBase
- Inherits:
-
Object
- Object
- Ast::Merge::EmitterBase
- Defined in:
- lib/ast/merge/emitter_base.rb
Overview
Base class for emitters that convert AST structures back to text. Provides common functionality for tracking indentation, managing output lines, and handling comments.
Subclasses implement format-specific emission methods (e.g., emit_pair for JSON, emit_variable_assignment for Bash, etc.)
Ownership boundary:
- shared structural recomposition and attachment preservation belong here
- syntax-aware normalization and serializer polish belong in the relevant family layer or concrete emitter subclass unless they prove reusable across unrelated formats
Defined Under Namespace
Classes: UnsupportedCommentNodeError
Instance Attribute Summary collapse
-
#indent_level ⇒ Integer
readonly
Current indentation level.
-
#indent_size ⇒ Integer
readonly
Spaces per indent level.
-
#lines ⇒ Array<String>
readonly
Output lines.
Instance Method Summary collapse
-
#blank_lines?(candidate_lines) ⇒ Boolean
Check whether every provided line is blank.
-
#clear ⇒ Object
Clear the emitter state.
-
#clear_subclass_state ⇒ Object
Hook for subclasses to clear their own state.
-
#dedent ⇒ Object
Decrease indentation level.
-
#emit_blank_line ⇒ Object
Emit a blank line.
-
#emit_comment(text, inline: false) ⇒ Object
Emit a comment using the emitter's native syntax.
-
#emit_comment_attachment(attachment, leading: true, inline: false, trailing: false, orphan: false, source_lines: nil) ⇒ Object
Emit selected regions from a shared comment attachment.
-
#emit_comment_region(region, inline: nil, source_lines: nil) ⇒ Object
Emit a shared normalized comment region.
-
#emit_layout_attachment(attachment, leading: true, trailing: false, source_lines: nil, retained_owners: nil, removed_owners: nil, leading_last_emitted_source_line: nil, trailing_last_emitted_source_line: nil) ⇒ Hash{Symbol=>Integer}
Emit selected leading/trailing layout gaps from an attachment.
-
#emit_layout_gap(gap, owner: nil, source_lines: nil, retained_owners: nil, removed_owners: nil, last_emitted_source_line: nil) ⇒ Integer?
Emit a shared layout gap when the requesting owner controls output.
-
#emit_leading_comments(comments) ⇒ Object
Emit leading comments from CommentTracker.
-
#emit_raw_lines(raw_lines) ⇒ Object
Emit raw lines as-is (for preserving exact formatting).
-
#emit_tracked_comment(comment) ⇒ Object
Emit a comment from CommentTracker hash Subclasses should override this to handle format-specific comment syntax.
-
#ends_with_blank_line? ⇒ Boolean
Check whether the current emitter output ends with a blank line.
-
#indent ⇒ Object
Increase indentation level.
-
#initialize(indent_size: 2, **options) ⇒ EmitterBase
constructor
Initialize a new emitter.
-
#initialize_subclass_state(**options) ⇒ Object
Hook for subclasses to initialize their own state.
-
#to_s ⇒ String
Get the output as a single string Subclasses may override to customize output format (e.g., to_json, to_yaml).
Constructor Details
#initialize(indent_size: 2, **options) ⇒ EmitterBase
Initialize a new emitter
42 43 44 45 46 47 |
# File 'lib/ast/merge/emitter_base.rb', line 42 def initialize(indent_size: 2, **) @lines = [] @indent_level = 0 @indent_size = indent_size initialize_subclass_state(**) end |
Instance Attribute Details
#indent_level ⇒ Integer (readonly)
Returns Current indentation level.
33 34 35 |
# File 'lib/ast/merge/emitter_base.rb', line 33 def indent_level @indent_level end |
#indent_size ⇒ Integer (readonly)
Returns Spaces per indent level.
36 37 38 |
# File 'lib/ast/merge/emitter_base.rb', line 36 def indent_size @indent_size end |
#lines ⇒ Array<String> (readonly)
Returns Output lines.
30 31 32 |
# File 'lib/ast/merge/emitter_base.rb', line 30 def lines @lines end |
Instance Method Details
#blank_lines?(candidate_lines) ⇒ Boolean
Check whether every provided line is blank.
241 242 243 |
# File 'lib/ast/merge/emitter_base.rb', line 241 def blank_lines?(candidate_lines) Array(candidate_lines).all? { |line| line.to_s.strip.empty? } end |
#clear ⇒ Object
Clear the emitter state
253 254 255 256 257 |
# File 'lib/ast/merge/emitter_base.rb', line 253 def clear @lines = [] @indent_level = 0 clear_subclass_state end |
#clear_subclass_state ⇒ Object
Hook for subclasses to clear their own state
260 261 262 |
# File 'lib/ast/merge/emitter_base.rb', line 260 def clear_subclass_state # Override in subclasses if needed end |
#dedent ⇒ Object
Decrease indentation level
270 271 272 |
# File 'lib/ast/merge/emitter_base.rb', line 270 def dedent @indent_level -= 1 if @indent_level.positive? end |
#emit_blank_line ⇒ Object
Emit a blank line
56 57 58 |
# File 'lib/ast/merge/emitter_base.rb', line 56 def emit_blank_line @lines << '' end |
#emit_comment(text, inline: false) ⇒ Object
Emit a comment using the emitter's native syntax. Subclasses should override this to support full-line and inline emission.
82 83 84 |
# File 'lib/ast/merge/emitter_base.rb', line 82 def emit_comment(text, inline: false) raise NotImplementedError, 'Subclasses must implement emit_comment' end |
#emit_comment_attachment(attachment, leading: true, inline: false, trailing: false, orphan: false, source_lines: nil) ⇒ Object
Emit selected regions from a shared comment attachment.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/ast/merge/emitter_base.rb', line 120 def (, leading: true, inline: false, trailing: false, orphan: false, source_lines: nil) return unless return unless .respond_to?(:leading_region) && .respond_to?(:inline_region) regions = [] regions << .leading_region if leading && .leading_region regions << .inline_region if inline && .inline_region if trailing && .respond_to?(:trailing_region) && .trailing_region regions << .trailing_region end regions.concat(Array(.orphan_regions)) if orphan && .respond_to?(:orphan_regions) previous_region_end_line = nil regions.each do |region| current_region_start_line = region.respond_to?(:start_line) ? region.start_line : nil emit_region_gap_lines(previous_region_end_line, current_region_start_line, source_lines) emit_comment_region(region, inline: region.respond_to?(:inline?) ? region.inline? : nil, source_lines: source_lines) previous_region_end_line = region.respond_to?(:end_line) ? region.end_line : previous_region_end_line end end |
#emit_comment_region(region, inline: nil, source_lines: nil) ⇒ Object
Emit a shared normalized comment region.
Preserves explicit blank-line nodes and can also recreate blank gaps between
comment lines by consulting original source lines when those gaps are not
already represented as Comment::Empty nodes.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/ast/merge/emitter_base.rb', line 95 def emit_comment_region(region, inline: nil, source_lines: nil) return unless region return unless region.respond_to?(:nodes) return if region.respond_to?(:empty?) && region.empty? inline = region.inline? if inline.nil? && region.respond_to?(:inline?) return emit_inline_comment_region(region) if inline previous_line = nil comment_region_nodes(region).each do |node| current_line = comment_region_line_number(node) emit_region_gap_lines(previous_line, current_line, source_lines) emit_comment_node(node) previous_line = current_line end end |
#emit_layout_attachment(attachment, leading: true, trailing: false, source_lines: nil, retained_owners: nil, removed_owners: nil, leading_last_emitted_source_line: nil, trailing_last_emitted_source_line: nil) ⇒ Hash{Symbol=>Integer}
Emit selected leading/trailing layout gaps from an attachment.
Works with both Layout::Attachment and Comment::Attachment because both expose owner/leading_gap/trailing_gap.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/ast/merge/emitter_base.rb', line 186 def (, leading: true, trailing: false, source_lines: nil, retained_owners: nil, removed_owners: nil, leading_last_emitted_source_line: nil, trailing_last_emitted_source_line: nil) return {} unless unless .respond_to?(:owner) && .respond_to?(:leading_gap) && .respond_to?(:trailing_gap) return {} end emitted_lines = {} if leading && .leading_gap emitted_lines[:leading] = emit_layout_gap( .leading_gap, owner: .owner, source_lines: source_lines, retained_owners: retained_owners, removed_owners: removed_owners, last_emitted_source_line: leading_last_emitted_source_line ) end if trailing && .trailing_gap emitted_lines[:trailing] = emit_layout_gap( .trailing_gap, owner: .owner, source_lines: source_lines, retained_owners: retained_owners, removed_owners: removed_owners, last_emitted_source_line: trailing_last_emitted_source_line ) end emitted_lines.compact end |
#emit_layout_gap(gap, owner: nil, source_lines: nil, retained_owners: nil, removed_owners: nil, last_emitted_source_line: nil) ⇒ Integer?
Emit a shared layout gap when the requesting owner controls output.
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/ast/merge/emitter_base.rb', line 152 def emit_layout_gap(gap, owner: nil, source_lines: nil, retained_owners: nil, removed_owners: nil, last_emitted_source_line: nil) return unless gap emitting_owner = owner || gap.effective_controller(retained_owners: retained_owners, removed_owners: removed_owners) || gap.controller return unless emitting_owner return unless gap.controls_output_for?(emitting_owner, retained_owners: retained_owners, removed_owners: removed_owners) start_line = if last_emitted_source_line [gap.start_line, last_emitted_source_line + 1].max else gap.start_line end return if start_line > gap.end_line emit_layout_gap_lines(gap, source_lines: source_lines, line_numbers: start_line..gap.end_line) end |
#emit_leading_comments(comments) ⇒ Object
Emit leading comments from CommentTracker
63 64 65 66 67 |
# File 'lib/ast/merge/emitter_base.rb', line 63 def emit_leading_comments(comments) comments.each do |comment| emit_tracked_comment(comment) end end |
#emit_raw_lines(raw_lines) ⇒ Object
Emit raw lines as-is (for preserving exact formatting)
223 224 225 |
# File 'lib/ast/merge/emitter_base.rb', line 223 def emit_raw_lines(raw_lines) raw_lines.each { |line| @lines << line.chomp } end |
#emit_tracked_comment(comment) ⇒ Object
Emit a comment from CommentTracker hash Subclasses should override this to handle format-specific comment syntax
73 74 75 |
# File 'lib/ast/merge/emitter_base.rb', line 73 def emit_tracked_comment(comment) raise NotImplementedError, 'Subclasses must implement emit_tracked_comment' end |
#ends_with_blank_line? ⇒ Boolean
Check whether the current emitter output ends with a blank line.
248 249 250 |
# File 'lib/ast/merge/emitter_base.rb', line 248 def ends_with_blank_line? @lines.any? && blank_lines?([@lines.last]) end |
#indent ⇒ Object
Increase indentation level
265 266 267 |
# File 'lib/ast/merge/emitter_base.rb', line 265 def indent @indent_level += 1 end |
#initialize_subclass_state(**options) ⇒ Object
Hook for subclasses to initialize their own state
51 52 53 |
# File 'lib/ast/merge/emitter_base.rb', line 51 def initialize_subclass_state(**) # Override in subclasses if needed end |
#to_s ⇒ String
Get the output as a single string Subclasses may override to customize output format (e.g., to_json, to_yaml)
231 232 233 234 235 |
# File 'lib/ast/merge/emitter_base.rb', line 231 def to_s content = @lines.join("\n") content += "\n" unless content.empty? || content.end_with?("\n") content end |