markly-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,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ module Merge
5
+ # Orchestrates the smart merge process for Markdown files using Markly.
6
+ #
7
+ # This is a thin wrapper around Markdown::Merge::SmartMerger that:
8
+ # - Forces the :markly backend
9
+ # - Sets markly-specific defaults (freeze token, inner_merge_code_blocks)
10
+ # - Exposes markly-specific options (flags, extensions)
11
+ #
12
+ # @example Basic merge (destination customizations preserved)
13
+ # merger = SmartMerger.new(template_content, dest_content)
14
+ # result = merger.merge
15
+ # if result.success?
16
+ # File.write("output.md", result.content)
17
+ # end
18
+ #
19
+ # @example Template updates win
20
+ # merger = SmartMerger.new(
21
+ # template_content,
22
+ # dest_content,
23
+ # preference: :template,
24
+ # add_template_only_nodes: true
25
+ # )
26
+ # result = merger.merge
27
+ #
28
+ # @example Custom signature matching
29
+ # sig_gen = ->(node) {
30
+ # canonical_type = Ast::Merge::NodeTyping.merge_type_for(node) || node.type
31
+ # if canonical_type == :heading
32
+ # [:heading, node.header_level] # Match by level only, not content
33
+ # else
34
+ # node # Fall through to default
35
+ # end
36
+ # }
37
+ # merger = SmartMerger.new(
38
+ # template_content,
39
+ # dest_content,
40
+ # signature_generator: sig_gen
41
+ # )
42
+ #
43
+ # @example Disable inner-merge for code blocks
44
+ # merger = SmartMerger.new(
45
+ # template_content,
46
+ # dest_content,
47
+ # inner_merge_code_blocks: false
48
+ # )
49
+ #
50
+ # @see Markdown::Merge::SmartMerger Underlying implementation
51
+ class SmartMerger < Markdown::Merge::SmartMerger
52
+ # Creates a new SmartMerger for intelligent Markdown file merging.
53
+ #
54
+ # @param template_content [String] Template Markdown source code
55
+ # @param dest_content [String] Destination Markdown source code
56
+ #
57
+ # @param signature_generator [Proc, nil] Optional proc to generate custom node signatures.
58
+ # The proc receives a node (wrapped with canonical merge_type) and should return one of:
59
+ # - An array representing the node's signature
60
+ # - `nil` to indicate the node should have no signature
61
+ # - The original node to fall through to default signature computation
62
+ #
63
+ # @param preference [Symbol] Controls which version to use when nodes
64
+ # have matching signatures but different content:
65
+ # - `:destination` (default) - Use destination version (preserves customizations)
66
+ # - `:template` - Use template version (applies updates)
67
+ #
68
+ # @param add_template_only_nodes [Boolean] Controls whether to add nodes that only
69
+ # exist in template:
70
+ # - `false` (default) - Skip template-only nodes
71
+ # - `true` - Add template-only nodes to result
72
+ #
73
+ # @param inner_merge_code_blocks [Boolean, CodeBlockMerger] Controls inner-merge for
74
+ # fenced code blocks:
75
+ # - `true` (default for markly-merge) - Enable inner-merge using default CodeBlockMerger
76
+ # - `false` - Disable inner-merge (use standard conflict resolution)
77
+ # - `CodeBlockMerger` instance - Use custom CodeBlockMerger
78
+ #
79
+ # @param freeze_token [String] Token to use for freeze block markers.
80
+ # Default: "markly-merge"
81
+ # Looks for: <!-- markly-merge:freeze --> / <!-- markly-merge:unfreeze -->
82
+ #
83
+ # @param flags [Integer] Markly parse flags (e.g., Markly::FOOTNOTES | Markly::SMART).
84
+ # Default: Markly::DEFAULT
85
+ #
86
+ # @param extensions [Array<Symbol>] Markly extensions to enable (e.g., [:table, :strikethrough])
87
+ # Available extensions: :table, :strikethrough, :autolink, :tagfilter, :tasklist
88
+ #
89
+ # @param match_refiner [#call, nil] Optional match refiner for fuzzy matching of
90
+ # unmatched nodes. Default: nil (fuzzy matching disabled).
91
+ #
92
+ # @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
93
+ # for per-node-type merge preferences.
94
+ # @param options [Hash] Additional options for forward compatibility
95
+ #
96
+ # @raise [TemplateParseError] If template has syntax errors
97
+ # @raise [DestinationParseError] If destination has syntax errors
98
+ def initialize(
99
+ template_content,
100
+ dest_content,
101
+ signature_generator: nil,
102
+ preference: :destination,
103
+ add_template_only_nodes: false,
104
+ inner_merge_code_blocks: DEFAULT_INNER_MERGE_CODE_BLOCKS,
105
+ freeze_token: DEFAULT_FREEZE_TOKEN,
106
+ flags: ::Markly::DEFAULT,
107
+ extensions: [:table],
108
+ match_refiner: nil,
109
+ node_typing: nil,
110
+ **options
111
+ )
112
+ super(
113
+ template_content,
114
+ dest_content,
115
+ backend: :markly,
116
+ signature_generator: signature_generator,
117
+ preference: preference,
118
+ add_template_only_nodes: add_template_only_nodes,
119
+ inner_merge_code_blocks: inner_merge_code_blocks,
120
+ freeze_token: freeze_token,
121
+ match_refiner: match_refiner,
122
+ node_typing: node_typing,
123
+ flags: flags,
124
+ extensions: extensions,
125
+ **options
126
+ )
127
+ end
128
+
129
+ # Returns the TemplateParseError class to use.
130
+ #
131
+ # @return [Class] Markly::Merge::TemplateParseError
132
+ def template_parse_error_class
133
+ TemplateParseError
134
+ end
135
+
136
+ # Returns the DestinationParseError class to use.
137
+ #
138
+ # @return [Class] Markly::Merge::DestinationParseError
139
+ def destination_parse_error_class
140
+ DestinationParseError
141
+ end
142
+
143
+ # Create a FileAnalysis instance for parsing.
144
+ #
145
+ # @param content [String] Markdown content to analyze
146
+ # @param options [Hash] Analysis options
147
+ # @return [Markly::Merge::FileAnalysis] File analysis instance
148
+ def create_file_analysis(content, **opts)
149
+ FileAnalysis.new(
150
+ content,
151
+ freeze_token: opts[:freeze_token],
152
+ signature_generator: opts[:signature_generator],
153
+ flags: opts[:flags] || ::Markly::DEFAULT,
154
+ extensions: opts[:extensions] || [:table],
155
+ )
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ module Merge
5
+ # Version information for Markly::Merge
6
+ module Version
7
+ # Current version of the markly-merge gem
8
+ VERSION = "1.0.0"
9
+ end
10
+ VERSION = Version::VERSION # traditional location
11
+ end
12
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Hard dependency - ensures markly gem is installed
4
+ require "markly"
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 Markly
16
+ # Smart merging for Markdown files using Markly AST.
17
+ #
18
+ # Markly::Merge provides intelligent merging of Markdown files by:
19
+ # - Parsing Markdown into AST using Markly (cmark-gfm) 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 markly gem
26
+ # - Sets markly-specific defaults (freeze token, inner_merge_code_blocks)
27
+ # - Exposes markly-specific options (flags, extensions)
28
+ # - Maintains API compatibility for existing users
29
+ #
30
+ # @example Basic merge
31
+ # merger = Markly::Merge::SmartMerger.new(template, destination)
32
+ # result = merger.merge
33
+ # puts result.content if result.success?
34
+ #
35
+ # @example With freeze blocks
36
+ # # In your Markdown file:
37
+ # # <!-- markly-merge:freeze -->
38
+ # # ## Custom Section
39
+ # # This content is preserved during merges.
40
+ # # <!-- markly-merge:unfreeze -->
41
+ #
42
+ # @see SmartMerger Main entry point for merging
43
+ # @see Markdown::Merge::SmartMerger Underlying implementation
44
+ module Merge
45
+ # Base error class for Markly::Merge
46
+ # Inherits from Markdown::Merge::Error for consistency across merge gems.
47
+ class Error < Markdown::Merge::Error; end
48
+
49
+ # Raised when a Markdown file has parsing errors.
50
+ # Inherits from Markdown::Merge::ParseError for consistency across merge gems.
51
+ class ParseError < Markdown::Merge::ParseError; end
52
+
53
+ # Raised when the template file has syntax errors.
54
+ class TemplateParseError < ParseError; end
55
+
56
+ # Raised when the destination file has syntax errors.
57
+ class DestinationParseError < ParseError; end
58
+
59
+ # Default freeze token for markly-merge
60
+ # @return [String]
61
+ DEFAULT_FREEZE_TOKEN = "markly-merge"
62
+
63
+ # Default inner_merge_code_blocks setting for markly-merge
64
+ # @return [Boolean]
65
+ DEFAULT_INNER_MERGE_CODE_BLOCKS = true
66
+
67
+ # Re-export shared classes from markdown-merge
68
+ # Re-export shared classes from markdown-merge
69
+ FileAligner = Markdown::Merge::FileAligner
70
+ ConflictResolver = Markdown::Merge::ConflictResolver
71
+ MergeResult = Markdown::Merge::MergeResult
72
+ TableMatchAlgorithm = Markdown::Merge::TableMatchAlgorithm
73
+ TableMatchRefiner = Markdown::Merge::TableMatchRefiner
74
+ CodeBlockMerger = Markdown::Merge::CodeBlockMerger
75
+ NodeTypeNormalizer = Markdown::Merge::NodeTypeNormalizer
76
+
77
+ autoload :DebugLogger, "markly/merge/debug_logger"
78
+ autoload :FreezeNode, "markly/merge/freeze_node"
79
+ autoload :FileAnalysis, "markly/merge/file_analysis"
80
+ autoload :SmartMerger, "markly/merge/smart_merger"
81
+ autoload :Backend, "markly/merge/backend"
82
+
83
+ # Eagerly load and register backend when this module is loaded
84
+ # This ensures the backend is available for tree_haver before any parsing happens
85
+ class << self
86
+ def ensure_backend_loaded!
87
+ Backend # Access constant to trigger autoload
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ # Ensure backend is loaded and registered
94
+ Markly::Merge.ensure_backend_loaded!
95
+
96
+ # Register with ast-merge's MergeGemRegistry for RSpec dependency tags
97
+ # Only register if MergeGemRegistry is loaded (i.e., in test environment)
98
+ if defined?(Ast::Merge::RSpec::MergeGemRegistry)
99
+ Ast::Merge::RSpec::MergeGemRegistry.register(
100
+ :markly_merge,
101
+ require_path: "markly/merge",
102
+ merger_class: "Markly::Merge::SmartMerger",
103
+ test_source: "# Test\n\nParagraph",
104
+ category: :markdown,
105
+ )
106
+ end
107
+
108
+ Markly::Merge::Version.class_eval do
109
+ extend VersionGem::Basic
110
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # For technical reasons, if we move to Zeitwerk, this cannot be require_relative.
4
+ # See: https://github.com/fxn/zeitwerk#for_gem_extension
5
+ # Hook for other libraries to load this library (e.g. via bundler)
6
+ require "markly/merge"
@@ -0,0 +1,106 @@
1
+ module Markly
2
+ module Merge
3
+ VERSION: String
4
+
5
+ # Base error class for Markly::Merge
6
+ class Error < Markdown::Merge::Error
7
+ end
8
+
9
+ # Raised when a Markdown file has parsing errors
10
+ class ParseError < Markdown::Merge::ParseError
11
+ def initialize: (?String? message, ?content: String?, ?errors: Array[untyped]) -> void
12
+ end
13
+
14
+ # Raised when the template file has syntax errors
15
+ class TemplateParseError < ParseError
16
+ end
17
+
18
+ # Raised when the destination file has syntax errors
19
+ class DestinationParseError < ParseError
20
+ end
21
+
22
+ # Re-exported shared classes from markdown-merge
23
+ FileAligner: singleton(Markdown::Merge::FileAligner)
24
+ ConflictResolver: singleton(Markdown::Merge::ConflictResolver)
25
+ MergeResult: singleton(Markdown::Merge::MergeResult)
26
+ TableMatchAlgorithm: singleton(Markdown::Merge::TableMatchAlgorithm)
27
+ TableMatchRefiner: singleton(Markdown::Merge::TableMatchRefiner)
28
+ CodeBlockMerger: singleton(Markdown::Merge::CodeBlockMerger)
29
+
30
+ # Debug logging utility for Markly::Merge
31
+ module DebugLogger
32
+ extend Ast::Merge::DebugLogger
33
+
34
+ def self.env_var_name: () -> String
35
+ def self.env_var_name=: (String name) -> String
36
+ def self.log_prefix: () -> String
37
+ def self.log_prefix=: (String prefix) -> String
38
+ def self.enabled?: () -> bool
39
+ def self.debug: (*untyped args) -> void
40
+ def self.info: (*untyped args) -> void
41
+ def self.warning: (*untyped args) -> void
42
+ def self.time: [T] (String label) { () -> T } -> T
43
+ def self.log_node: (untyped node, ?label: String) -> void
44
+ end
45
+
46
+ # Represents a frozen block of Markdown content
47
+ class FreezeNode < Ast::Merge::FreezeNodeBase
48
+ def initialize: (
49
+ start_line: Integer,
50
+ end_line: Integer,
51
+ content: String,
52
+ start_marker: String,
53
+ end_marker: String,
54
+ ?nodes: Array[untyped],
55
+ ?reason: String?
56
+ ) -> void
57
+
58
+ def signature: () -> Array[Symbol | String]
59
+ def full_text: () -> String
60
+ def line_count: () -> Integer
61
+ def contains_type?: (Symbol type_symbol) -> bool
62
+ def inspect: () -> String
63
+ end
64
+
65
+ # File analysis for Markdown files using Markly
66
+ class FileAnalysis < Markdown::Merge::FileAnalysisBase
67
+ DEFAULT_FREEZE_TOKEN: String
68
+
69
+ def initialize: (
70
+ String source,
71
+ ?freeze_token: String,
72
+ ?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
73
+ ?flags: Integer,
74
+ ?extensions: Array[Symbol]
75
+ ) -> void
76
+
77
+ def parse_document: (String source) -> untyped
78
+ def next_sibling: (untyped node) -> untyped?
79
+ def freeze_node_class: () -> Class
80
+ def parser_node?: (untyped value) -> bool
81
+ def fallthrough_node?: (untyped value) -> bool
82
+ def compute_parser_signature: (untyped node) -> Array[untyped]?
83
+ end
84
+
85
+ # Orchestrates the smart merge process for Markdown files using Markly
86
+ class SmartMerger < Markdown::Merge::SmartMergerBase
87
+ def initialize: (
88
+ String template_content,
89
+ String dest_content,
90
+ ?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
91
+ ?preference: Symbol,
92
+ ?add_template_only_nodes: bool,
93
+ ?inner_merge_code_blocks: (bool | Markdown::Merge::CodeBlockMerger),
94
+ ?freeze_token: String,
95
+ ?match_refiner: (^(Array[untyped], Array[untyped], Hash[Symbol, untyped]) -> Array[Markdown::Merge::MatchResult])?,
96
+ ?flags: Integer,
97
+ ?extensions: Array[Symbol]
98
+ ) -> void
99
+
100
+ def create_file_analysis: (String content, **untyped options) -> FileAnalysis
101
+ def node_to_source: (untyped node, FileAnalysis analysis) -> String
102
+ def template_parse_error_class: () -> Class
103
+ def destination_parse_error_class: () -> Class
104
+ end
105
+ end
106
+ end
data.tar.gz.sig ADDED
Binary file