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:

Instance Attribute Summary collapse

Attributes inherited from SmartMergerBase

#aligner, #code_block_merger, #dest_analysis, #node_typing, #resolver, #template_analysis

Instance Method Summary collapse

Methods inherited from SmartMergerBase

#merge, #merge_result, #stats

Constructor Details

#initialize(template_content, dest_content, backend: :auto, signature_generator: nil, preference: :destination, add_template_only_nodes: false, inner_merge_code_blocks: false, freeze_token: FileAnalysis::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: :auto)

    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: 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: FileAnalysis::DEFAULT_FREEZE_TOKEN)

    Token to use for freeze block markers. Default: “markdown-merge” Looks for: <!– markdown-merge:freeze –> / <!– markdown-merge:unfreeze –>

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



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

def initialize(
  template_content,
  dest_content,
  backend: :auto,
  signature_generator: nil,
  preference: :destination,
  add_template_only_nodes: false,
  inner_merge_code_blocks: false,
  freeze_token: FileAnalysis::DEFAULT_FREEZE_TOKEN,
  match_refiner: nil,
  node_typing: nil,
  **parser_options
)
  @requested_backend = backend
  @parser_options = 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,
    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)



63
64
65
# File 'lib/markdown/merge/smart_merger.rb', line 63

def backend
  @backend
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:



154
155
156
157
158
159
160
161
162
# File 'lib/markdown/merge/smart_merger.rb', line 154

def create_file_analysis(content, **opts)
  FileAnalysis.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



174
175
176
# File 'lib/markdown/merge/smart_merger.rb', line 174

def destination_parse_error_class
  DestinationParseError
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



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/markdown/merge/smart_merger.rb', line 186

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

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

  # 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



167
168
169
# File 'lib/markdown/merge/smart_merger.rb', line 167

def template_parse_error_class
  TemplateParseError
end