Class: Markdown::Merge::SmartMergerBase Abstract

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

Overview

This class is abstract.

Subclass and implement parser-specific methods

Base class for smart Markdown file merging.

Orchestrates the smart merge process for Markdown files using FileAnalysisBase, FileAligner, ConflictResolver, and MergeResult to merge two Markdown files intelligently. Freeze blocks marked with HTML comments are preserved exactly as-is.

Subclasses must implement:

  • #create_file_analysis(content, **options) - Create parser-specific FileAnalysis

  • #node_to_source(node, analysis) - Convert a node to source text

SmartMergerBase provides flexible configuration for different merge scenarios:

  • Preserve destination customizations (default)

  • Apply template updates

  • Add new sections from template

  • Inner-merge fenced code blocks using language-specific mergers (optional)

Examples:

Subclass implementation

class SmartMerger < Markdown::Merge::SmartMergerBase
  def create_file_analysis(content, **options)
    FileAnalysis.new(content, **options)
  end

  def node_to_source(node, analysis)
    case node
    when FreezeNode
      node.full_text
    else
      analysis.source_range(node.start_line, node.end_line)
    end
  end
end

See Also:

Direct Known Subclasses

SmartMerger

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template_content, dest_content, signature_generator: nil, preference: :destination, add_template_only_nodes: false, inner_merge_code_blocks: false, freeze_token: FileAnalysisBase::DEFAULT_FREEZE_TOKEN, match_refiner: nil, node_typing: nil, normalize_whitespace: false, rehydrate_link_references: false, **parser_options) ⇒ SmartMergerBase

Creates a new SmartMerger for intelligent Markdown file merging.

Parameters:

  • template_content (String)

    Template Markdown source code

  • dest_content (String)

    Destination Markdown source code

  • signature_generator (Proc, nil) (defaults to: nil)

    Optional proc to generate custom node signatures. The proc receives a node and should return one of:

    • An array representing the node’s signature

    • nil to indicate the node should have no signature

    • The original node to fall through to default signature computation

  • preference (Symbol, Hash) (defaults to: :destination)

    Controls which version to use when nodes have matching signatures but different content:

    • :destination (default) - Use destination version (preserves customizations)

    • :template - Use template version (applies updates)

    • Hash for per-type preferences: ‘{ default: :destination, gem_table: :template }`

  • add_template_only_nodes (Boolean, #call) (defaults to: false)

    Controls whether to add nodes that only exist in template:

    • false (default) - Skip template-only nodes

    • true - Add all template-only nodes to result

    • Callable (Proc/Lambda) - Called with (node, entry) for each template-only node. Return truthy to add the node, falsey to skip it. @example Filter to only add gem family link refs

      add_template_only_nodes: ->(node, entry) {
        sig = entry[:signature]
        sig.is_a?(Array) && sig.first == :gem_family
      }
      
  • inner_merge_code_blocks (Boolean, CodeBlockMerger) (defaults to: false)

    Controls inner-merge for fenced code blocks:

    • true - Enable inner-merge using default CodeBlockMerger

    • false (default) - Disable inner-merge (use standard conflict resolution)

    • CodeBlockMerger instance - Use custom CodeBlockMerger

  • freeze_token (String) (defaults to: FileAnalysisBase::DEFAULT_FREEZE_TOKEN)

    Token to use for freeze block markers. Default: “markdown-merge”

  • match_refiner (#call, nil) (defaults to: nil)

    Optional match refiner for fuzzy matching of unmatched nodes. Default: nil (fuzzy matching disabled). Set to TableMatchRefiner.new to enable fuzzy table matching.

  • node_typing (Hash{Symbol,String => #call}, nil) (defaults to: nil)

    Node typing configuration for per-node-type merge preferences. Maps node type names to callables that can wrap nodes with custom merge_types for use with Hash-based preference. @example

    node_typing = {
      table: ->(node) {
        text = node.to_plaintext
        if text.include?("tree_haver")
          Ast::Merge::NodeTyping.with_merge_type(node, :gem_family_table)
        else
          node
        end
      }
    }
    merger = SmartMerger.new(template, dest,
      node_typing: node_typing,
      preference: { default: :destination, gem_family_table: :template })
    
  • normalize_whitespace (Boolean, Symbol) (defaults to: false)

    Whitespace normalization mode:

    • false (default) - No normalization

    • true or :basic - Collapse excessive blank lines (3+ → 2)

    • :link_refs - Basic + remove blank lines between consecutive link reference definitions

    • :strict - All normalizations (same as :link_refs currently)

  • rehydrate_link_references (Boolean) (defaults to: false)

    If true, convert inline links/images to reference-style when a matching link reference definition exists. Default: false

  • parser_options (Hash)

    Additional parser-specific options

Raises:



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/markdown/merge/smart_merger_base.rb', line 135

def initialize(
  template_content,
  dest_content,
  signature_generator: nil,
  preference: :destination,
  add_template_only_nodes: false,
  inner_merge_code_blocks: false,
  freeze_token: FileAnalysisBase::DEFAULT_FREEZE_TOKEN,
  match_refiner: nil,
  node_typing: nil,
  normalize_whitespace: false,
  rehydrate_link_references: false,
  **parser_options
)
  @preference = preference
  @add_template_only_nodes = add_template_only_nodes
  @match_refiner = match_refiner
  @node_typing = node_typing
  @normalize_whitespace = normalize_whitespace
  @rehydrate_link_references = rehydrate_link_references

  # Validate node_typing if provided
  Ast::Merge::NodeTyping.validate!(node_typing) if node_typing

  # Set up code block merger
  @code_block_merger = case inner_merge_code_blocks
  when true
    CodeBlockMerger.new
  when false
    nil
  when CodeBlockMerger
    inner_merge_code_blocks
  else
    raise ArgumentError, "inner_merge_code_blocks must be true, false, or a CodeBlockMerger instance"
  end

  # Parse template
  begin
    @template_analysis = create_file_analysis(
      template_content,
      freeze_token: freeze_token,
      signature_generator: signature_generator,
      **parser_options,
    )
  rescue StandardError => e
    raise template_parse_error_class.new(errors: [e])
  end

  # Parse destination
  begin
    @dest_analysis = create_file_analysis(
      dest_content,
      freeze_token: freeze_token,
      signature_generator: signature_generator,
      **parser_options,
    )
  rescue StandardError => e
    raise destination_parse_error_class.new(errors: [e])
  end

  @aligner = FileAligner.new(@template_analysis, @dest_analysis, match_refiner: @match_refiner)
  @resolver = ConflictResolver.new(
    preference: @preference,
    template_analysis: @template_analysis,
    dest_analysis: @dest_analysis,
  )
end

Instance Attribute Details

#alignerFileAligner (readonly)

Returns Aligner for finding matches and differences.

Returns:

  • (FileAligner)

    Aligner for finding matches and differences



51
52
53
# File 'lib/markdown/merge/smart_merger_base.rb', line 51

def aligner
  @aligner
end

#code_block_mergerCodeBlockMerger? (readonly)

Returns Merger for fenced code blocks.

Returns:



57
58
59
# File 'lib/markdown/merge/smart_merger_base.rb', line 57

def code_block_merger
  @code_block_merger
end

#dest_analysisFileAnalysisBase (readonly)

Returns Analysis of the destination file.

Returns:



48
49
50
# File 'lib/markdown/merge/smart_merger_base.rb', line 48

def dest_analysis
  @dest_analysis
end

#node_typingHash{Symbol,String => #call}? (readonly)

Returns Node typing configuration.

Returns:

  • (Hash{Symbol,String => #call}, nil)

    Node typing configuration



60
61
62
# File 'lib/markdown/merge/smart_merger_base.rb', line 60

def node_typing
  @node_typing
end

#resolverConflictResolver (readonly)

Returns Resolver for handling conflicting content.

Returns:



54
55
56
# File 'lib/markdown/merge/smart_merger_base.rb', line 54

def resolver
  @resolver
end

#template_analysisFileAnalysisBase (readonly)

Returns Analysis of the template file.

Returns:



45
46
47
# File 'lib/markdown/merge/smart_merger_base.rb', line 45

def template_analysis
  @template_analysis
end

Instance Method Details

#create_file_analysis(content, **options) ⇒ FileAnalysisBase

This method is abstract.

Subclasses must implement this method

Create a FileAnalysis instance for the given content.

Parameters:

  • content (String)

    Markdown content to analyze

  • options (Hash)

    Analysis options

Returns:

Raises:

  • (NotImplementedError)


209
210
211
# File 'lib/markdown/merge/smart_merger_base.rb', line 209

def create_file_analysis(content, **options)
  raise NotImplementedError, "#{self.class} must implement #create_file_analysis"
end

#destination_parse_error_classClass

Returns the DestinationParseError class to use.

Subclasses should override to return their parser-specific error class.

Returns:

  • (Class)

    DestinationParseError class



227
228
229
# File 'lib/markdown/merge/smart_merger_base.rb', line 227

def destination_parse_error_class
  DestinationParseError
end

#mergeString

Perform the merge operation and return the merged content as a string.

Returns:

  • (String)

    The merged Markdown content



234
235
236
# File 'lib/markdown/merge/smart_merger_base.rb', line 234

def merge
  merge_result.content
end

#merge_resultMergeResult

Perform the merge operation and return the full MergeResult object.

Returns:

  • (MergeResult)

    The merge result containing merged content and metadata



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
273
274
275
276
277
278
279
# File 'lib/markdown/merge/smart_merger_base.rb', line 241

def merge_result
  return @merge_result if @merge_result

  @merge_result = DebugLogger.time("SmartMergerBase#merge") do
    alignment = DebugLogger.time("SmartMergerBase#align") do
      @aligner.align
    end

    DebugLogger.debug("Alignment complete", {
      total_entries: alignment.size,
      matches: alignment.count { |e| e[:type] == :match },
      template_only: alignment.count { |e| e[:type] == :template_only },
      dest_only: alignment.count { |e| e[:type] == :dest_only },
    })

    # Process alignment using OutputBuilder
    builder, stats, frozen_blocks, conflicts = DebugLogger.time("SmartMergerBase#process") do
      process_alignment(alignment)
    end

    # Get content from OutputBuilder
    content = builder.to_s

    # Collect problems from post-processing
    problems = DocumentProblems.new

    # Apply post-processing transformations
    content, problems = apply_post_processing(content, problems)

    # Get final content from OutputBuilder
    MergeResult.new(
      content: content,
      conflicts: conflicts,
      frozen_blocks: frozen_blocks,
      stats: stats,
      problems: problems,
    )
  end
end

#statsHash

Get merge statistics (convenience method).

Returns:

  • (Hash)

    Statistics from the merge result



284
285
286
# File 'lib/markdown/merge/smart_merger_base.rb', line 284

def stats
  merge_result.stats
end

#template_parse_error_classClass

Returns the TemplateParseError class to use.

Subclasses should override to return their parser-specific error class.

Returns:

  • (Class)

    TemplateParseError class



218
219
220
# File 'lib/markdown/merge/smart_merger_base.rb', line 218

def template_parse_error_class
  TemplateParseError
end