commonmarker-merge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hard dependency - ensures commonmarker gem is installed
4
+ require "commonmarker"
5
+
6
+ # External gems
7
+ require "version_gem"
8
+
9
+ # Shared merge infrastructure (includes tree_haver)
10
+ require "markdown/merge"
11
+
12
+ # This gem
13
+ require_relative "merge/version"
14
+
15
+ module Commonmarker
16
+ # Smart merging for Markdown files using CommonMarker AST.
17
+ #
18
+ # Commonmarker::Merge provides intelligent merging of Markdown files by:
19
+ # - Parsing Markdown into AST using CommonMarker via tree_haver
20
+ # - Matching structural elements (headings, paragraphs, lists, etc.) between files
21
+ # - Preserving frozen sections marked with HTML comments
22
+ # - Resolving conflicts based on configurable preferences
23
+ #
24
+ # This is a thin wrapper around Markdown::Merge that:
25
+ # - Provides hard dependency on the commonmarker gem
26
+ # - Sets commonmarker-specific defaults (freeze token, inner_merge_code_blocks)
27
+ # - Maintains API compatibility for existing users
28
+ #
29
+ # @example Basic merge
30
+ # merger = Commonmarker::Merge::SmartMerger.new(template, destination)
31
+ # result = merger.merge
32
+ # puts result.content if result.success?
33
+ #
34
+ # @example With freeze blocks
35
+ # # In your Markdown file:
36
+ # # <!-- commonmarker-merge:freeze -->
37
+ # # ## Custom Section
38
+ # # This content is preserved during merges.
39
+ # # <!-- commonmarker-merge:unfreeze -->
40
+ #
41
+ # @see SmartMerger Main entry point for merging
42
+ # @see Markdown::Merge::SmartMerger Underlying implementation
43
+ module Merge
44
+ # Base error class for Commonmarker::Merge
45
+ # Inherits from Markdown::Merge::Error for consistency across merge gems.
46
+ class Error < Markdown::Merge::Error; end
47
+
48
+ # Raised when a Markdown file has parsing errors.
49
+ # Inherits from Markdown::Merge::ParseError for consistency across merge gems.
50
+ class ParseError < Markdown::Merge::ParseError; end
51
+
52
+ # Raised when the template file has syntax errors.
53
+ class TemplateParseError < ParseError; end
54
+
55
+ # Raised when the destination file has syntax errors.
56
+ class DestinationParseError < ParseError; end
57
+
58
+ # Default freeze token for commonmarker-merge
59
+ # @return [String]
60
+ DEFAULT_FREEZE_TOKEN = "commonmarker-merge"
61
+
62
+ # Default inner_merge_code_blocks setting for commonmarker-merge
63
+ # @return [Boolean]
64
+ DEFAULT_INNER_MERGE_CODE_BLOCKS = false
65
+
66
+ # Re-export shared classes from markdown-merge
67
+ FileAligner = Markdown::Merge::FileAligner
68
+ ConflictResolver = Markdown::Merge::ConflictResolver
69
+ MergeResult = Markdown::Merge::MergeResult
70
+ TableMatchAlgorithm = Markdown::Merge::TableMatchAlgorithm
71
+ TableMatchRefiner = Markdown::Merge::TableMatchRefiner
72
+ CodeBlockMerger = Markdown::Merge::CodeBlockMerger
73
+ NodeTypeNormalizer = Markdown::Merge::NodeTypeNormalizer
74
+
75
+ autoload :DebugLogger, "commonmarker/merge/debug_logger"
76
+ autoload :FreezeNode, "commonmarker/merge/freeze_node"
77
+ autoload :FileAnalysis, "commonmarker/merge/file_analysis"
78
+ autoload :SmartMerger, "commonmarker/merge/smart_merger"
79
+ autoload :Backend, "commonmarker/merge/backend"
80
+
81
+ class << self
82
+ # Eagerly load and register backend when this module is loaded
83
+ # This ensures the backend is available for tree_haver before any parsing happens
84
+ def ensure_backend_loaded!
85
+ Backend # Access constant to trigger autoload
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # Ensure backend is loaded and registered
92
+ Commonmarker::Merge.ensure_backend_loaded!
93
+
94
+ # Register with ast-merge's MergeGemRegistry for RSpec dependency tags
95
+ # Only register if MergeGemRegistry is loaded (i.e., in test environment)
96
+ if defined?(Ast::Merge::RSpec::MergeGemRegistry)
97
+ Ast::Merge::RSpec::MergeGemRegistry.register(
98
+ :commonmarker_merge,
99
+ require_path: "commonmarker/merge",
100
+ merger_class: "Commonmarker::Merge::SmartMerger",
101
+ test_source: "# Test\n\nParagraph",
102
+ category: :markdown,
103
+ )
104
+ end
105
+
106
+ Commonmarker::Merge::Version.class_eval do
107
+ extend VersionGem::Basic
108
+ end
@@ -0,0 +1,4 @@
1
+ # For technical reasons, if we move to Zeitwerk, this cannot be require_relative.
2
+ # See: https://github.com/fxn/zeitwerk#for_gem_extension
3
+ # Hook for other libraries to load this library (e.g. via bundler)
4
+ require "commonmarker/merge"
@@ -0,0 +1,48 @@
1
+ # Type signatures for Commonmarker::Merge::ConflictResolver
2
+ #
3
+ # Resolves conflicts between matching Markdown elements.
4
+ # Determines which version to use based on configured preference.
5
+
6
+ module Commonmarker
7
+ module Merge
8
+ class ConflictResolver
9
+ # Decision constants
10
+ DECISION_DESTINATION: Symbol
11
+ DECISION_TEMPLATE: Symbol
12
+ DECISION_ADDED: Symbol
13
+ DECISION_FROZEN: Symbol
14
+
15
+ # Merge preference
16
+ attr_reader preference: Symbol
17
+
18
+ # Template file analysis
19
+ attr_reader template_analysis: FileAnalysis
20
+
21
+ # Destination file analysis
22
+ attr_reader dest_analysis: FileAnalysis
23
+
24
+ # Initialize a conflict resolver
25
+ def initialize: (
26
+ preference: Symbol,
27
+ template_analysis: FileAnalysis,
28
+ dest_analysis: FileAnalysis
29
+ ) -> void
30
+
31
+ # Resolve a conflict between template and destination nodes
32
+ def resolve: (
33
+ untyped template_node,
34
+ untyped dest_node,
35
+ template_index: Integer,
36
+ dest_index: Integer
37
+ ) -> Hash[Symbol, untyped]
38
+
39
+ private
40
+
41
+ # Check if two nodes have identical content
42
+ def content_identical?: (untyped template_node, untyped dest_node) -> bool
43
+
44
+ # Convert a node to its source text
45
+ def node_to_text: (untyped node, FileAnalysis analysis) -> String
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,36 @@
1
+ # Type signatures for Commonmarker::Merge::DebugLogger
2
+ #
3
+ # Debug logging utility for Commonmarker::Merge operations.
4
+ # Extends Ast::Merge::DebugLogger module.
5
+
6
+ module Commonmarker
7
+ module Merge
8
+ module DebugLogger
9
+ # Configured environment variable name
10
+ def self.env_var_name: () -> String
11
+ def self.env_var_name=: (String) -> String
12
+
13
+ # Configured log prefix
14
+ def self.log_prefix: () -> String
15
+ def self.log_prefix=: (String) -> String
16
+
17
+ # Check if debug mode is enabled
18
+ def self.enabled?: () -> bool
19
+
20
+ # Log a debug message with optional context
21
+ def self.debug: (String message, ?Hash[Symbol, untyped] context) -> void
22
+
23
+ # Log an info message
24
+ def self.info: (String message) -> void
25
+
26
+ # Log a warning message
27
+ def self.warning: (String message) -> void
28
+
29
+ # Time a block and log the duration
30
+ def self.time: [T] (String operation) { () -> T } -> T
31
+
32
+ # Extract node information for logging
33
+ def self.extract_node_info: (untyped node) -> Hash[Symbol, untyped]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ # Type signatures for Commonmarker::Merge::FileAligner
2
+ #
3
+ # Aligns Markdown block elements between template and destination files.
4
+ # Uses structural signatures to match elements.
5
+
6
+ module Commonmarker
7
+ module Merge
8
+ class FileAligner
9
+ # Template file analysis
10
+ attr_reader template_analysis: FileAnalysis
11
+
12
+ # Destination file analysis
13
+ attr_reader dest_analysis: FileAnalysis
14
+
15
+ # Initialize a file aligner
16
+ def initialize: (FileAnalysis template_analysis, FileAnalysis dest_analysis) -> void
17
+
18
+ # Perform alignment between template and destination statements
19
+ def align: () -> Array[Hash[Symbol, untyped]]
20
+
21
+ private
22
+
23
+ # Build a map from signatures to statement indices
24
+ def build_signature_map: (Array[untyped] statements, FileAnalysis analysis) -> Hash[Array[untyped], Array[Integer]]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,95 @@
1
+ # Type signatures for Commonmarker::Merge::FileAnalysis
2
+ #
3
+ # File analysis for Markdown files using CommonMarker.
4
+ # Parses Markdown and extracts block elements and freeze blocks.
5
+
6
+ module Commonmarker
7
+ module Merge
8
+ class FileAnalysis
9
+ # Default freeze token for identifying freeze blocks
10
+ DEFAULT_FREEZE_TOKEN: String
11
+
12
+ # The root document node
13
+ attr_reader document: untyped
14
+
15
+ # Source Markdown content
16
+ attr_reader source: String
17
+
18
+ # Lines of source code
19
+ attr_reader lines: Array[String]
20
+
21
+ # Token used to mark freeze blocks
22
+ attr_reader freeze_token: String
23
+
24
+ # Custom signature generator
25
+ attr_reader signature_generator: (^(untyped) -> (Array[untyped] | nil | untyped))?
26
+
27
+ # All statements (nodes and freeze blocks)
28
+ attr_reader statements: Array[untyped]
29
+
30
+ # Initialize file analysis
31
+ def initialize: (
32
+ String source,
33
+ ?freeze_token: String,
34
+ ?signature_generator: (^(untyped) -> (Array[untyped] | nil | untyped))?,
35
+ ?options: Hash[Symbol, untyped]
36
+ ) -> void
37
+
38
+ # Check if parse was successful
39
+ def valid?: () -> bool
40
+
41
+ # Get freeze blocks
42
+ def freeze_blocks: () -> Array[FreezeNode]
43
+
44
+ # Get a specific line (1-indexed)
45
+ def line_at: (Integer line_number) -> String?
46
+
47
+ # Get a normalized line
48
+ def normalized_line: (Integer line_number) -> String?
49
+
50
+ # Get signature at index
51
+ def signature_at: (Integer index) -> Array[untyped]?
52
+
53
+ # Generate signature for a node
54
+ def generate_signature: (untyped node) -> Array[untyped]?
55
+
56
+ # Compute default signature for a node
57
+ def compute_node_signature: (untyped node) -> Array[untyped]?
58
+
59
+ # Compute signature for CommonMarker node
60
+ def compute_commonmarker_signature: (untyped node) -> Array[untyped]?
61
+
62
+ # Extract all text content from a node
63
+ def extract_text_content: (untyped node) -> String
64
+
65
+ # Get source text for a range of lines
66
+ def source_range: (Integer start_line, Integer end_line) -> String
67
+
68
+ private
69
+
70
+ # Count children of a node
71
+ def count_children: (untyped node) -> Integer
72
+
73
+ # Get node name
74
+ def node_name: (untyped node) -> String?
75
+
76
+ # Extract all nodes and integrate freeze blocks
77
+ def extract_and_integrate_all_nodes: () -> Array[untyped]
78
+
79
+ # Collect top-level nodes from document
80
+ def collect_top_level_nodes: () -> Array[untyped]
81
+
82
+ # Find freeze markers in source
83
+ def find_freeze_markers: () -> Array[Hash[Symbol, untyped]]
84
+
85
+ # Build freeze blocks from markers
86
+ def build_freeze_blocks: (Array[Hash[Symbol, untyped]] markers) -> Array[FreezeNode]
87
+
88
+ # Create a freeze block from start and end markers
89
+ def create_freeze_block: (Hash[Symbol, untyped] start_marker, Hash[Symbol, untyped] end_marker) -> FreezeNode
90
+
91
+ # Integrate nodes with freeze blocks
92
+ def integrate_nodes_with_freeze_blocks: (Array[FreezeNode] freeze_blocks) -> Array[untyped]
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,65 @@
1
+ # Type signatures for Commonmarker::Merge::FreezeNode
2
+ #
3
+ # Represents a frozen block of Markdown content that should be
4
+ # preserved during merges. Marked with HTML comments.
5
+
6
+ module Commonmarker
7
+ module Merge
8
+ class FreezeNode < Ast::Merge::FreezeNode
9
+ # Regex pattern for HTML comment freeze markers
10
+ HTML_COMMENT_PATTERN: Regexp
11
+
12
+ # Build regex pattern for freeze markers
13
+ def self.pattern_for: (Symbol pattern_type, ?String? token) -> Regexp
14
+
15
+ # Start line number (1-indexed)
16
+ attr_reader start_line: Integer
17
+
18
+ # End line number (1-indexed)
19
+ attr_reader end_line: Integer
20
+
21
+ # Raw Markdown content within the freeze block
22
+ attr_reader content: String
23
+
24
+ # The start marker comment
25
+ attr_reader start_marker: String
26
+
27
+ # The end marker comment
28
+ attr_reader end_marker: String
29
+
30
+ # Parsed nodes within the freeze block
31
+ attr_reader nodes: Array[untyped]
32
+
33
+ # Initialize a new FreezeNode
34
+ def initialize: (
35
+ start_line: Integer,
36
+ end_line: Integer,
37
+ content: String,
38
+ start_marker: String,
39
+ end_marker: String,
40
+ ?nodes: Array[untyped],
41
+ ?reason: String?
42
+ ) -> void
43
+
44
+ # Generate a signature for matching this freeze block
45
+ def signature: () -> Array[Symbol | String]
46
+
47
+ # Get the full text including markers
48
+ def full_text: () -> String
49
+
50
+ # Get line count of the freeze block
51
+ def line_count: () -> Integer
52
+
53
+ # Check if block contains a specific node type
54
+ def contains_type?: (Symbol `type`) -> bool
55
+
56
+ # String representation for debugging
57
+ def inspect: () -> String
58
+
59
+ private
60
+
61
+ # Extract reason from the start marker comment
62
+ def extract_reason_from_marker: (String? marker) -> String?
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,59 @@
1
+ # Type signatures for Commonmarker::Merge::MergeResult
2
+ #
3
+ # Represents the result of a Markdown merge operation.
4
+ # Contains merged content and metadata about the operation.
5
+
6
+ module Commonmarker
7
+ module Merge
8
+ class MergeResult < Ast::Merge::MergeResult
9
+ # The merged Markdown content
10
+ attr_reader content: String?
11
+
12
+ # List of conflicts encountered during merge
13
+ attr_reader conflicts: Array[Hash[Symbol, untyped]]
14
+
15
+ # List of frozen blocks preserved during merge
16
+ attr_reader frozen_blocks: Array[Hash[Symbol, untyped]]
17
+
18
+ # Statistics about the merge operation
19
+ attr_reader stats: Hash[Symbol, untyped]
20
+
21
+ # Initialize a new MergeResult
22
+ def initialize: (
23
+ content: String?,
24
+ ?conflicts: Array[Hash[Symbol, untyped]],
25
+ ?frozen_blocks: Array[Hash[Symbol, untyped]],
26
+ ?stats: Hash[Symbol, untyped]
27
+ ) -> void
28
+
29
+ # Check if merge was successful
30
+ def success?: () -> bool
31
+
32
+ # Check if there are unresolved conflicts
33
+ def conflicts?: () -> bool
34
+
35
+ # Check if any frozen blocks were preserved
36
+ def has_frozen_blocks?: () -> bool
37
+
38
+ # Get count of nodes added during merge
39
+ def nodes_added: () -> Integer
40
+
41
+ # Get count of nodes removed during merge
42
+ def nodes_removed: () -> Integer
43
+
44
+ # Get count of nodes modified during merge
45
+ def nodes_modified: () -> Integer
46
+
47
+ # Get count of frozen blocks preserved
48
+ def frozen_count: () -> Integer
49
+
50
+ # String representation for debugging
51
+ def inspect: () -> String
52
+
53
+ private
54
+
55
+ # Default statistics hash
56
+ def default_stats: () -> Hash[Symbol, untyped]
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ # Type signatures for Commonmarker::Merge::SmartMerger
2
+ #
3
+ # Orchestrates the smart merge process for Markdown files.
4
+ # Main entry point for intelligent Markdown merging.
5
+
6
+ module Commonmarker
7
+ module Merge
8
+ class SmartMerger
9
+ # Analysis of the template file
10
+ attr_reader template_analysis: FileAnalysis
11
+
12
+ # Analysis of the destination file
13
+ attr_reader dest_analysis: FileAnalysis
14
+
15
+ # Aligner for finding matches and differences
16
+ attr_reader aligner: FileAligner
17
+
18
+ # Resolver for handling conflicting content
19
+ attr_reader resolver: ConflictResolver
20
+
21
+ # Initialize a SmartMerger
22
+ def initialize: (
23
+ String template_content,
24
+ String dest_content,
25
+ ?signature_generator: (^(untyped) -> (Array[untyped] | nil | untyped))?,
26
+ ?signature_match_preference: Symbol,
27
+ ?add_template_only_nodes: bool,
28
+ ?freeze_token: String,
29
+ ?options: Hash[Symbol, untyped]
30
+ ) -> void
31
+
32
+ # Perform the merge operation
33
+ def merge: () -> MergeResult
34
+
35
+ private
36
+
37
+ # Process alignment entries and build result
38
+ def process_alignment: (Array[Hash[Symbol, untyped]] alignment) -> [Array[String], Hash[Symbol, untyped], Array[Hash[Symbol, untyped]], Array[Hash[Symbol, untyped]]]
39
+
40
+ # Process a matched node pair
41
+ def process_match: (Hash[Symbol, untyped] entry, Hash[Symbol, untyped] stats) -> [String?, Hash[Symbol, untyped]?]
42
+
43
+ # Process a template-only node
44
+ def process_template_only: (Hash[Symbol, untyped] entry, Hash[Symbol, untyped] stats) -> String?
45
+
46
+ # Process a destination-only node
47
+ def process_dest_only: (Hash[Symbol, untyped] entry, Hash[Symbol, untyped] stats) -> [String?, Hash[Symbol, untyped]?]
48
+
49
+ # Convert a node to its source text
50
+ def node_to_source: (untyped node, FileAnalysis analysis) -> String
51
+ end
52
+ end
53
+ end