Class: Markdown::Merge::CodeBlockMerger

Inherits:
Object
  • Object
show all
Defined in:
lib/markdown/merge/code_block_merger.rb

Overview

Merges fenced code blocks using language-specific *-merge gems.

When two code blocks with the same signature are matched, this class delegates the merge to the appropriate language-specific merger:

  • Ruby code → prism-merge

  • YAML code → psych-merge

  • JSON code → json-merge

  • TOML code → toml-merge

Examples:

Basic usage

merger = CodeBlockMerger.new
result = merger.merge_code_blocks(template_node, dest_node, preference: :destination)
if result[:merged]
  puts result[:content]
else
  # Fall back to standard resolution
end

With custom mergers

merger = CodeBlockMerger.new(
  mergers: {
    "ruby" => ->(template, dest, pref) { MyCustomRubyMerger.merge(template, dest, pref) },
  }
)

See Also:

Constant Summary collapse

DEFAULT_MERGERS =

Default language-to-merger mapping Each merger is a lambda that takes (template_content, dest_content, preference) and returns { merged: true/false, content: String, stats: Hash } :nocov: integration - DEFAULT_MERGERS lambdas require external gems

{
  # Ruby code blocks
  "ruby" => ->(template, dest, preference, **opts) {
    require "prism/merge"
    CodeBlockMerger.merge_with_prism(template, dest, preference, **opts)
  },
  "rb" => ->(template, dest, preference, **opts) {
    require "prism/merge"
    CodeBlockMerger.merge_with_prism(template, dest, preference, **opts)
  },

  # YAML code blocks
  "yaml" => ->(template, dest, preference, **opts) {
    require "psych/merge"
    CodeBlockMerger.merge_with_psych(template, dest, preference, **opts)
  },
  "yml" => ->(template, dest, preference, **opts) {
    require "psych/merge"
    CodeBlockMerger.merge_with_psych(template, dest, preference, **opts)
  },

  # JSON code blocks
  "json" => ->(template, dest, preference, **opts) {
    require "json/merge"
    CodeBlockMerger.merge_with_json(template, dest, preference, **opts)
  },

  # TOML code blocks
  "toml" => ->(template, dest, preference, **opts) {
    require "toml/merge"
    CodeBlockMerger.merge_with_toml(template, dest, preference, **opts)
  },
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mergers: {}, enabled: true) ⇒ CodeBlockMerger

Creates a new CodeBlockMerger.

Parameters:

  • mergers (Hash<String, Proc>) (defaults to: {})

    Custom language-to-merger mapping. Mergers are merged with defaults, allowing selective overrides.

  • enabled (Boolean) (defaults to: true)

    Whether to enable inner-merge (default: true)



83
84
85
86
# File 'lib/markdown/merge/code_block_merger.rb', line 83

def initialize(mergers: {}, enabled: true)
  @mergers = DEFAULT_MERGERS.merge(mergers)
  @enabled = enabled
end

Instance Attribute Details

#enabledBoolean (readonly)

Returns Whether inner-merge is enabled.

Returns:

  • (Boolean)

    Whether inner-merge is enabled



76
77
78
# File 'lib/markdown/merge/code_block_merger.rb', line 76

def enabled
  @enabled
end

#mergersHash<String, Proc> (readonly)

Returns Language to merger mapping.

Returns:

  • (Hash<String, Proc>)

    Language to merger mapping



73
74
75
# File 'lib/markdown/merge/code_block_merger.rb', line 73

def mergers
  @mergers
end

Class Method Details

.merge_with_json(template, dest, preference, **opts) ⇒ Hash

Note:

Errors are handled by merge_code_blocks when called via DEFAULT_MERGERS

Merge JSON code using json-merge.

Parameters:

  • template (String)

    Template JSON code

  • dest (String)

    Destination JSON code

  • preference (Symbol)

    :destination or :template

Returns:

  • (Hash)

    Merge result

Raises:

  • (Json::Merge::ParseError)

    If template or dest has syntax errors



260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/markdown/merge/code_block_merger.rb', line 260

def merge_with_json(template, dest, preference, **opts)
  merger = ::Json::Merge::SmartMerger.new(
    template,
    dest,
    preference: preference,
    add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
  )

  {
    merged: true,
    content: merger.merge,
    stats: merger.stats,
  }
end

.merge_with_prism(template, dest, preference, **opts) ⇒ Hash

Note:

Errors are handled by merge_code_blocks when called via DEFAULT_MERGERS

Merge Ruby code using prism-merge.

Parameters:

  • template (String)

    Template Ruby code

  • dest (String)

    Destination Ruby code

  • preference (Symbol)

    :destination or :template

Returns:

  • (Hash)

    Merge result

Raises:

  • (Prism::Merge::ParseError)

    If template or dest has syntax errors



214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/markdown/merge/code_block_merger.rb', line 214

def merge_with_prism(template, dest, preference, **opts)
  merger = ::Prism::Merge::SmartMerger.new(
    template,
    dest,
    preference: preference,
    add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
  )

  {
    merged: true,
    content: merger.merge,
    stats: merger.stats,
  }
end

.merge_with_psych(template, dest, preference, **opts) ⇒ Hash

Note:

Errors are handled by merge_code_blocks when called via DEFAULT_MERGERS

Merge YAML code using psych-merge.

Parameters:

  • template (String)

    Template YAML code

  • dest (String)

    Destination YAML code

  • preference (Symbol)

    :destination or :template

Returns:

  • (Hash)

    Merge result

Raises:

  • (Psych::Merge::ParseError)

    If template or dest has syntax errors



237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/markdown/merge/code_block_merger.rb', line 237

def merge_with_psych(template, dest, preference, **opts)
  merger = ::Psych::Merge::SmartMerger.new(
    template,
    dest,
    preference: preference,
    add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
  )

  {
    merged: true,
    content: merger.merge,
    stats: merger.stats,
  }
end

.merge_with_toml(template, dest, preference, **opts) ⇒ Hash

Note:

Errors are handled by merge_code_blocks when called via DEFAULT_MERGERS

Merge TOML code using toml-merge.

Parameters:

  • template (String)

    Template TOML code

  • dest (String)

    Destination TOML code

  • preference (Symbol)

    :destination or :template

Returns:

  • (Hash)

    Merge result

Raises:

  • (Toml::Merge::ParseError)

    If template or dest has syntax errors



283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/markdown/merge/code_block_merger.rb', line 283

def merge_with_toml(template, dest, preference, **opts)
  merger = ::Toml::Merge::SmartMerger.new(
    template,
    dest,
    preference: preference,
    add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
  )

  {
    merged: true,
    content: merger.merge,
    stats: merger.stats,
  }
end

Instance Method Details

#merge_code_blocks(template_node, dest_node, preference:, **opts) ⇒ Hash

Merge two code blocks using the appropriate language-specific merger.

Parameters:

  • template_node (Object)

    Template code block node

  • dest_node (Object)

    Destination code block node

  • preference (Symbol)

    :destination or :template

  • opts (Hash)

    Additional options passed to the merger

Returns:

  • (Hash)

    { merged: Boolean, content: String, stats: Hash }



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/markdown/merge/code_block_merger.rb', line 106

def merge_code_blocks(template_node, dest_node, preference:, **opts)
  return not_merged("inner-merge disabled") unless @enabled

  language = extract_language(template_node) || extract_language(dest_node)
  return not_merged("no language specified") unless language

  merger = @mergers[language.downcase]
  return not_merged("no merger for language: #{language}") unless merger

  template_content = extract_content(template_node)
  dest_content = extract_content(dest_node)

  # If content is identical, no need to merge
  if template_content == dest_content
    return {
      merged: true,
      content: rebuild_code_block(language, dest_content, dest_node),
      stats: {decision: :identical},
    }
  end

  begin
    result = merger.call(template_content, dest_content, preference, **opts)
    if result[:merged]
      {
        merged: true,
        content: rebuild_code_block(language, result[:content], dest_node),
        stats: result[:stats] || {},
      }
    else
      not_merged(result[:reason] || "merger declined")
    end
  rescue LoadError => e
    not_merged("merger gem not available: #{e.message}")
  rescue TreeHaver::Error => e
    # TreeHaver::NotAvailable and TreeHaver::Error inherit from Exception (not StandardError)
    # for safety reasons related to backend conflicts. We catch them here to handle
    # gracefully when a backend isn't properly configured.
    not_merged("backend not available: #{e.message}")
  rescue StandardError => e
    # :nocov: defensive - Prism::Merge::ParseError handling when prism/merge is loaded
    # Check for Prism::Merge::ParseError if prism/merge is loaded
    if defined?(::Prism::Merge::ParseError) && e.is_a?(::Prism::Merge::ParseError)
      not_merged("Ruby parse error: #{e.message}")
    else
      not_merged("merge failed: #{e.class}: #{e.message}")
    end
    # :nocov:
  end
end

#supports_language?(language) ⇒ Boolean

Check if inner-merge is available for a language.

Parameters:

  • language (String)

    The language identifier from fence_info

Returns:

  • (Boolean)

    true if a merger exists for this language



92
93
94
95
96
97
# File 'lib/markdown/merge/code_block_merger.rb', line 92

def supports_language?(language)
  return false unless @enabled
  return false if language.nil? || language.empty?

  @mergers.key?(language.downcase)
end