Class: Markdown::Merge::SmartMerger

Inherits:
SmartMergerBase show all
Defined in:
lib/markdown/merge/smart_merger.rb

Overview

Orchestrates the smart merge process for Markdown files using tree_haver backends.

Extends SmartMergerBase with backend-agnostic parsing via tree_haver. Supports both Commonmarker and Markly backends.

Uses FileAnalysis, FileAligner, ConflictResolver, and MergeResult to merge two Markdown files intelligently. Freeze blocks marked with HTML comments are preserved exactly as-is.

SmartMerger 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:

Basic merge (destination customizations preserved)

merger = SmartMerger.new(template_content, dest_content)
result = merger.merge
if result.success?
  File.write("output.md", result.content)
end

With specific backend

merger = SmartMerger.new(
  template_content,
  dest_content,
  backend: :markly
)
result = merger.merge

Template updates win

merger = SmartMerger.new(
  template_content,
  dest_content,
  preference: :template,
  add_template_only_nodes: true
)
result = merger.merge

Custom signature matching

sig_gen = ->(node) {
  canonical_type = Ast::Merge::NodeTyping.merge_type_for(node) || node.type
  if canonical_type == :heading
    [:heading, node.header_level]  # Match by level only, not content
  else
    node  # Fall through to default
  end
}
merger = SmartMerger.new(
  template_content,
  dest_content,
  signature_generator: sig_gen
)

See Also:

Constant Summary collapse

VALID_BACKENDS =
%i[auto commonmarker markly kramdown].freeze

Instance Attribute Summary collapse

Attributes inherited from SmartMergerBase

#aligner, #code_block_merger, #corruption_handling, #dest_analysis, #node_typing, #resolution_mode, #resolver, #runtime_session, #template_analysis, #unresolved_policy

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from SmartMergerBase

#default_match_refiner, #merge, #merge_result, #merge_with_debug, #stats

Constructor Details

#initialize(template_content, dest_content, backend: self.class.default_backend, signature_generator: nil, preference: :destination, add_template_only_nodes: false, inner_merge_code_blocks: self.class.default_inner_merge_code_blocks, inner_merge_lists: false, remove_template_missing_nodes: false, freeze_token: self.class.default_freeze_token, match_refiner: nil, node_typing: nil, **parser_options) ⇒ SmartMerger

Creates a new SmartMerger for intelligent Markdown file merging.

Parameters:

  • template_content (String)

    Template Markdown source code

  • dest_content (String)

    Destination Markdown source code

  • backend (Symbol) (defaults to: self.class.default_backend)

    Backend to use for parsing:

    • :commonmarker - Use Commonmarker (comrak Rust parser)
    • :markly - Use Markly (cmark-gfm C library)
    • :auto (default) - Auto-detect available backend
  • signature_generator (Proc, nil) (defaults to: nil)

    Optional proc to generate custom node signatures. The proc receives a node (wrapped with canonical merge_type) 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) (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)
  • add_template_only_nodes (Boolean) (defaults to: false)

    Controls whether to add nodes that only exist in template:

    • false (default) - Skip template-only nodes
    • true - Add template-only nodes to result
  • inner_merge_code_blocks (Boolean, CodeBlockMerger) (defaults to: self.class.default_inner_merge_code_blocks)

    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
  • remove_template_missing_nodes (Boolean) (defaults to: false)

    Controls whether destination-only structural nodes should be removed instead of preserved. Standalone HTML comment-only fragments, freeze blocks, and link reference definitions remain preserved when enabled.

  • freeze_token (String) (defaults to: self.class.default_freeze_token)

    Token to use for freeze block markers. Default: "markdown-merge" Looks for: /

  • 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.

  • parser_options (Hash)

    Backend-specific parser options. For commonmarker: { options: {} } For markly: { flags: Markly::DEFAULT, extensions: [:table] }

Raises:



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
# File 'lib/markdown/merge/smart_merger.rb', line 151

def initialize(
  template_content,
  dest_content,
  backend: self.class.default_backend,
  signature_generator: nil,
  preference: :destination,
  add_template_only_nodes: false,
  inner_merge_code_blocks: self.class.default_inner_merge_code_blocks,
  inner_merge_lists: false,
  remove_template_missing_nodes: false,
  freeze_token: self.class.default_freeze_token,
  match_refiner: nil,
  node_typing: nil,
  **parser_options
)
  validate_backend!(backend)

  @requested_backend = backend
  @parser_options = self.class.default_parser_options.merge(parser_options)

  super(
    template_content,
    dest_content,
    signature_generator: signature_generator,
    preference: preference,
    add_template_only_nodes: add_template_only_nodes,
    inner_merge_code_blocks: inner_merge_code_blocks,
    inner_merge_lists: inner_merge_lists,
    remove_template_missing_nodes: remove_template_missing_nodes,
    freeze_token: freeze_token,
    match_refiner: match_refiner,
    node_typing: node_typing,
    # Pass through for FileAnalysis
    backend: backend,
    **parser_options,
  )

  # Capture the resolved backend from template analysis
  @backend = @template_analysis.backend
end

Instance Attribute Details

#backendSymbol (readonly)

Returns The backend being used (:commonmarker, :markly).

Returns:

  • (Symbol)

    The backend being used (:commonmarker, :markly)



95
96
97
# File 'lib/markdown/merge/smart_merger.rb', line 95

def backend
  @backend
end

Class Method Details

.default_backendObject



65
66
67
# File 'lib/markdown/merge/smart_merger.rb', line 65

def default_backend
  :auto
end

.default_freeze_tokenObject



69
70
71
# File 'lib/markdown/merge/smart_merger.rb', line 69

def default_freeze_token
  FileAnalysis::DEFAULT_FREEZE_TOKEN
end

.default_inner_merge_code_blocksObject



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

def default_inner_merge_code_blocks
  false
end

.default_parser_optionsObject



77
78
79
# File 'lib/markdown/merge/smart_merger.rb', line 77

def default_parser_options
  {}
end

.destination_parse_error_classObject



89
90
91
# File 'lib/markdown/merge/smart_merger.rb', line 89

def destination_parse_error_class
  DestinationParseError
end

.file_analysis_classObject



81
82
83
# File 'lib/markdown/merge/smart_merger.rb', line 81

def file_analysis_class
  FileAnalysis
end

.template_parse_error_classObject



85
86
87
# File 'lib/markdown/merge/smart_merger.rb', line 85

def template_parse_error_class
  TemplateParseError
end

Instance Method Details

#create_file_analysis(content, **opts) ⇒ FileAnalysis

Create a FileAnalysis instance for parsing.

Parameters:

  • content (String)

    Markdown content to analyze

  • options (Hash)

    Analysis options

Returns:



197
198
199
200
201
202
203
204
205
# File 'lib/markdown/merge/smart_merger.rb', line 197

def create_file_analysis(content, **opts)
  self.class.file_analysis_class.new(
    content,
    backend: opts[:backend] || @requested_backend,
    freeze_token: opts[:freeze_token],
    signature_generator: opts[:signature_generator],
    **@parser_options
  )
end

#destination_parse_error_classClass

Returns the DestinationParseError class to use.

Returns:

  • (Class)

    Markdown::Merge::DestinationParseError



217
218
219
# File 'lib/markdown/merge/smart_merger.rb', line 217

def destination_parse_error_class
  self.class.destination_parse_error_class
end

#node_to_source(node, analysis) ⇒ String

Convert a node to its source text.

Handles wrapped nodes from NodeTypeNormalizer, gap line nodes, and link definition nodes created during gap detection.

Parameters:

  • node (Object)

    Node to convert (may be wrapped)

  • analysis (FileAnalysis)

    Analysis for source lookup

Returns:

  • (String)

    Source text



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/markdown/merge/smart_merger.rb', line 229

def node_to_source(node, analysis)
  # Check for any FreezeNode type (base class or subclass)
  return node.full_text if node.is_a?(Ast::Merge::FreezeNodeBase)

  # Handle gap line nodes (created for blank lines and link definitions)
  return node.content if node.is_a?(LinkDefinitionNode) || node.is_a?(GapLineNode)

  # Unwrap if needed to access source_position
  raw_node = Ast::Merge::NodeTyping.unwrap(node)

  pos = raw_node.source_position
  start_line = pos&.dig(:start_line)
  end_line = pos&.dig(:end_line)

  # Fall back to to_commonmark if no position info
  return raw_node.to_commonmark unless start_line && end_line

  # Get source from line range
  source = analysis.source_range(start_line, end_line)

  # Handle Markly's buggy position reporting for :html nodes
  # where end_line < start_line results in empty source_range.
  # Fall back to to_commonmark in that case.
  if source.empty? && raw_node.respond_to?(:to_commonmark)
    raw_node.to_commonmark.chomp
  else
    source
  end
end

#template_parse_error_classClass

Returns the TemplateParseError class to use.

Returns:

  • (Class)

    Markdown::Merge::TemplateParseError



210
211
212
# File 'lib/markdown/merge/smart_merger.rb', line 210

def template_parse_error_class
  self.class.template_parse_error_class
end