markly-merge 7.0.0 → 7.1.3

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,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ module Merge
5
+ # Markly backend using the Markly gem (cmark-gfm C library)
6
+ #
7
+ # This backend wraps Markly, a Ruby gem that provides bindings to
8
+ # cmark-gfm, GitHub's fork of the CommonMark C library with extensions.
9
+ #
10
+ # @note This backend only parses Markdown source code
11
+ # @see https://github.com/ioquatix/markly Markly gem
12
+ #
13
+ # @example Basic usage
14
+ # parser = TreeHaver::Parser.new
15
+ # parser.language = Markly::Merge::Backend::Language.markdown(
16
+ # flags: Markly::DEFAULT,
17
+ # extensions: [:table, :strikethrough]
18
+ # )
19
+ # tree = parser.parse(markdown_source)
20
+ # root = tree.root_node
21
+ # puts root.type # => "document"
22
+ module Backend
23
+ Markdown::Merge::BackendSupport.install!(
24
+ backend_module: self,
25
+ backend_name: :markly,
26
+ gem_name: 'markly',
27
+ require_path: 'markly/merge',
28
+ capabilities: {
29
+ gfm_extensions: true
30
+ }
31
+ )
32
+
33
+ # Markly language wrapper
34
+ #
35
+ # Markly only parses Markdown. This class exists for API compatibility
36
+ # and to pass through Markly-specific options (flags, extensions).
37
+ #
38
+ # @example
39
+ # language = Markly::Merge::Backend::Language.markdown(
40
+ # flags: Markly::DEFAULT | Markly::FOOTNOTES,
41
+ # extensions: [:table, :strikethrough]
42
+ # )
43
+ # parser.language = language
44
+ class Language < ::TreeHaver::Base::Language
45
+ # Markly parse flags
46
+ # @return [Integer]
47
+ attr_reader :flags
48
+
49
+ # Markly extensions to enable
50
+ # @return [Array<Symbol>]
51
+ attr_reader :extensions
52
+
53
+ # Create a new Markly language instance
54
+ #
55
+ # @param name [Symbol] Language name (should be :markdown)
56
+ # @param flags [Integer] Markly parse flags (default: Markly::DEFAULT)
57
+ # @param extensions [Array<Symbol>] Extensions to enable (default: [:table])
58
+ # @param options [Hash] parsing options (reserved for future use)
59
+ def initialize(name = :markdown, flags: nil, extensions: [:table], options: {})
60
+ super(name, backend: :markly, options: options.merge({ flags: flags, extensions: extensions }))
61
+ @flags = flags # Will use Markly::DEFAULT if nil at parse time
62
+ @extensions = extensions
63
+
64
+ return if @name == :markdown
65
+
66
+ raise TreeHaver::NotAvailable,
67
+ 'Markly backend only supports Markdown parsing. ' \
68
+ "Got language: #{name.inspect}"
69
+ end
70
+
71
+ class << self
72
+ # Create a Markdown language instance
73
+ #
74
+ # @param flags [Integer] Markly parse flags
75
+ # @param extensions [Array<Symbol>] Extensions to enable
76
+ # @param options [Hash] parsing options (reserved for future use)
77
+ # @return [Language] Markdown language
78
+ def markdown(flags: nil, extensions: [:table], options: {})
79
+ new(:markdown, flags: flags, extensions: extensions, options: options)
80
+ end
81
+
82
+ # Load language from library path (API compatibility)
83
+ #
84
+ # @param _path [String] Ignored - Markly doesn't load external grammars
85
+ # @param symbol [String, nil] Ignored
86
+ # @param name [String, nil] Language name hint (defaults to :markdown)
87
+ # @return [Language] Markdown language
88
+ # @raise [TreeHaver::NotAvailable] if requested language is not Markdown
89
+ end
90
+
91
+ Markdown::Merge::BackendSupport.configure_markdown_only_language_class!(
92
+ self,
93
+ backend_label: 'Markly',
94
+ unsupported_language_message: lambda { |lang_name|
95
+ "Markly backend only supports Markdown, not #{lang_name}. " \
96
+ "Use a tree-sitter backend for #{lang_name} support."
97
+ }
98
+ )
99
+ end
100
+
101
+ # Markly parser wrapper
102
+ class Parser < ::TreeHaver::Base::Parser
103
+ # Create a new RBS parser instance
104
+ #
105
+ # @raise [TreeHaver::NotAvailable] if rbs gem is not available
106
+ def initialize
107
+ super()
108
+ raise TreeHaver::NotAvailable, 'markly gem not available' unless Backend.available?
109
+ end
110
+
111
+ # Set the language for this parser
112
+ #
113
+ # @param lang [Language, Symbol] RBS language (should be :rbs or Language instance)
114
+ # @return [void]
115
+ def language=(lang)
116
+ case lang
117
+ when Language
118
+ @language = lang
119
+ when Symbol, String
120
+ if lang.to_sym == :markdown
121
+ @language = Language.markdown
122
+ else
123
+ raise ArgumentError,
124
+ "Markly backend only supports Markdown parsing. Got: #{lang.inspect}"
125
+ end
126
+ else
127
+ raise ArgumentError,
128
+ "Expected Backend::Language or :markdown, got #{lang.class}"
129
+ end
130
+ end
131
+
132
+ # Parse Markdown source code
133
+ #
134
+ # @param source [String] Markdown source to parse
135
+ # @return [Tree] Parsed tree
136
+ def parse(source)
137
+ raise 'Language not set' unless language
138
+
139
+ Backend.available? or raise 'Markly not available'
140
+
141
+ flags = language.flags || ::Markly::DEFAULT
142
+ exts = language.extensions || [:table]
143
+ doc = ::Markly.parse(source, flags: flags, extensions: exts)
144
+ Tree.new(doc, source)
145
+ end
146
+ end
147
+
148
+ # Markly node wrapper
149
+ #
150
+ # Wraps Markly::Node to provide TreeHaver::Node-compatible interface.
151
+ class Node < ::TreeHaver::Base::Node
152
+ # Type normalization map (Markly → canonical)
153
+ TYPE_MAP = {
154
+ header: 'heading',
155
+ hrule: 'thematic_break',
156
+ html: 'html_block'
157
+ }.freeze
158
+
159
+ Markdown::Merge::BackendSupport.configure_node_link_and_navigation!(
160
+ self,
161
+ next_sibling_selector: :next,
162
+ prev_sibling_selector: :previous
163
+ )
164
+ Markdown::Merge::BackendSupport.configure_node_heading_and_code_block_helpers!(
165
+ self,
166
+ heading_matcher: ->(node) { node.raw_type == 'header' },
167
+ code_block_matcher: ->(node) { node.type == 'code_block' }
168
+ )
169
+
170
+ # Default source position for nodes that don't have position info
171
+ DEFAULT_SOURCE_POSITION = {
172
+ start_line: 1,
173
+ start_column: 1,
174
+ end_line: 1,
175
+ end_column: 1
176
+ }.freeze
177
+
178
+ # Get source position from the inner Markly node
179
+ #
180
+ # @return [Hash{Symbol => Integer}] Source position from Markly
181
+ # @api private
182
+ def inner_source_position
183
+ @inner_source_position ||= if inner_node.respond_to?(:source_position)
184
+ inner_node.source_position || DEFAULT_SOURCE_POSITION
185
+ else
186
+ DEFAULT_SOURCE_POSITION
187
+ end
188
+ end
189
+
190
+ # Get the node type as a string (normalized)
191
+ #
192
+ # @return [String] Node type
193
+ def type
194
+ raw = inner_node.type.to_s
195
+ TYPE_MAP[raw.to_sym]&.to_s || raw
196
+ end
197
+
198
+ # Get the raw (non-normalized) type
199
+ # @return [String]
200
+ def raw_type
201
+ inner_node.type.to_s
202
+ end
203
+
204
+ # Get the text content of this node
205
+ #
206
+ # @return [String] Node text
207
+ def text
208
+ if inner_node.respond_to?(:string_content)
209
+ content = inner_node.string_content.to_s
210
+ return content unless content.empty?
211
+ end
212
+
213
+ if inner_node.respond_to?(:to_plaintext)
214
+ begin
215
+ inner_node.to_plaintext
216
+ rescue StandardError
217
+ children.map(&:text).join
218
+ end
219
+ else
220
+ children.map(&:text).join
221
+ end
222
+ end
223
+
224
+ # Get child nodes (Markly uses first_child/next pattern)
225
+ #
226
+ # @return [Array<Node>] Child nodes
227
+ def children
228
+ result = []
229
+ child = begin
230
+ inner_node.first_child
231
+ rescue StandardError
232
+ nil
233
+ end
234
+ while child
235
+ result << Node.new(child, source: source, lines: lines)
236
+ child = begin
237
+ child.next
238
+ rescue StandardError
239
+ nil
240
+ end
241
+ end
242
+ result
243
+ end
244
+
245
+ # Position information
246
+
247
+ def start_byte
248
+ pos = inner_source_position
249
+ calculate_byte_offset(pos[:start_line] - 1, pos[:start_column] - 1)
250
+ end
251
+
252
+ def end_byte
253
+ pos = inner_source_position
254
+ calculate_byte_offset(pos[:end_line] - 1, pos[:end_column] - 1)
255
+ end
256
+
257
+ def start_point
258
+ pos = inner_source_position
259
+ { row: pos[:start_line] - 1, column: pos[:start_column] - 1 }
260
+ end
261
+
262
+ def end_point
263
+ pos = inner_source_position
264
+ { row: pos[:end_line] - 1, column: pos[:end_column] - 1 }
265
+ end
266
+
267
+ # Convert node to CommonMark/Markdown/HTML/plaintext
268
+ def to_commonmark
269
+ inner_node.to_commonmark
270
+ end
271
+
272
+ def to_markdown
273
+ inner_node.to_markdown
274
+ end
275
+
276
+ def to_plaintext
277
+ inner_node.to_plaintext
278
+ end
279
+
280
+ def to_html
281
+ inner_node.to_html
282
+ end
283
+
284
+ # Markly-specific methods
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ module Merge
5
+ CommentTracker = Markdown::Merge::CommentTracker
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ module Merge
5
+ # Debug logging utility for Markly::Merge operations.
6
+ #
7
+ # Extends Ast::Merge::DebugLogger to provide consistent logging
8
+ # across all merge gems. Logs are controlled via environment variables.
9
+ #
10
+ # @example Enable debug logging
11
+ # ENV["MARKLY_MERGE_DEBUG"] = "1"
12
+ # DebugLogger.debug("Parsing markdown", { file: "README.md" })
13
+ #
14
+ # @example Time an operation
15
+ # result = DebugLogger.time("parse") { Markly.parse(source) }
16
+ #
17
+ # @see Ast::Merge::DebugLogger Base module
18
+ module DebugLogger
19
+ Markdown::Merge::WrapperSupport.configure_debug_logger!(
20
+ debug_logger_module: self,
21
+ env_var_name: 'MARKLY_MERGE_DEBUG',
22
+ log_prefix: '[markly-merge]'
23
+ )
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ module Merge
5
+ # File analysis for Markdown files using Markly.
6
+ #
7
+ # This is a thin wrapper around Markdown::Merge::FileAnalysis that:
8
+ # - Forces the :markly backend
9
+ # - Sets the default freeze token to "markly-merge"
10
+ # - Exposes markly-specific options (flags, extensions)
11
+ #
12
+ # @example Basic usage
13
+ # analysis = FileAnalysis.new(markdown_source)
14
+ # analysis.statements.each do |node|
15
+ # puts "#{node.merge_type}: #{node.type}"
16
+ # end
17
+ #
18
+ # @example With custom freeze token
19
+ # analysis = FileAnalysis.new(source, freeze_token: "my-merge")
20
+ #
21
+ # @see Markdown::Merge::FileAnalysis Underlying implementation
22
+ class FileAnalysis < Markdown::Merge::FileAnalysis
23
+ # Default freeze token for markly-merge
24
+ # @return [String]
25
+ DEFAULT_FREEZE_TOKEN = 'markly-merge'
26
+
27
+ Markdown::Merge::WrapperSupport.configure_file_analysis_subclass!(
28
+ self,
29
+ default_backend: :markly,
30
+ default_parser_options: lambda do
31
+ {
32
+ flags: ::Markly::DEFAULT,
33
+ extensions: [:table]
34
+ }
35
+ end
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ module Merge
5
+ FreezeNode = Markdown::Merge::FreezeNode
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ module Merge
5
+ # Thin Markly-specific wrapper for Markdown::Merge::PartialTemplateMerger.
6
+ #
7
+ # Forces the :markly backend and the markly-merge default freeze token while
8
+ # inheriting all comment-handling and merge behavior from markdown-merge.
9
+ class PartialTemplateMerger < Markdown::Merge::PartialTemplateMerger
10
+ Result = Markdown::Merge::PartialTemplateMerger::Result
11
+
12
+ Markdown::Merge::WrapperSupport.configure_partial_template_merger_subclass!(
13
+ self,
14
+ default_backend: :markly,
15
+ file_analysis_class: -> { FileAnalysis },
16
+ smart_merger_class: -> { SmartMerger }
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markly
4
+ # Source-preserving Markdown provider integration for Markly.
5
+ module Merge
6
+ # Native Markly backend configuration for the shared provider engine.
7
+ class Provider < Markdown::Merge::SourcePreservingProvider
8
+ # rubocop:disable Metrics/MethodLength -- constructor declares the complete backend boundary
9
+ def initialize
10
+ super(
11
+ provider_id: 'ruby.markdown.markly',
12
+ role: :backend,
13
+ backend: Markdown::Merge::ProviderBackend.new(
14
+ id: :markly,
15
+ package: PACKAGE_NAME,
16
+ dialects: %i[markdown commonmark],
17
+ parser: lambda { |source|
18
+ parser = Backend::Parser.new
19
+ parser.language = Backend::Language.markdown
20
+ parser.parse(source)
21
+ },
22
+ headings: Markdown::Merge::ProviderBackend.method(:native_headings)
23
+ )
24
+ )
25
+ end
26
+ # rubocop:enable Metrics/MethodLength
27
+ end
28
+
29
+ class << self
30
+ def merge_provider
31
+ @merge_provider ||= Provider.new
32
+ end
33
+
34
+ def register_provider!(replace: false)
35
+ return unless Ast::Merge.respond_to?(:register_provider)
36
+
37
+ Ast::Merge.register_provider(merge_provider, replace: replace)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,69 @@
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
+ Markdown::Merge::WrapperSupport.configure_smart_merger_subclass!(
53
+ self,
54
+ default_backend: :markly,
55
+ default_freeze_token: -> { DEFAULT_FREEZE_TOKEN },
56
+ default_inner_merge_code_blocks: -> { DEFAULT_INNER_MERGE_CODE_BLOCKS },
57
+ default_parser_options: lambda do
58
+ {
59
+ flags: ::Markly::DEFAULT,
60
+ extensions: [:table]
61
+ }
62
+ end,
63
+ file_analysis_class: -> { FileAnalysis },
64
+ template_parse_error_class: -> { TemplateParseError },
65
+ destination_parse_error_class: -> { DestinationParseError }
66
+ )
67
+ end
68
+ end
69
+ end
@@ -2,10 +2,12 @@
2
2
 
3
3
  module Markly
4
4
  module Merge
5
+ # Version namespace for this gem.
5
6
  module Version
6
- VERSION = "7.0.0"
7
+ # Current gem version.
8
+ VERSION = '7.1.3'
7
9
  end
8
-
9
- VERSION = Version::VERSION
10
+ # Current gem version exposed at the traditional constant location.
11
+ VERSION = Version::VERSION # Traditional Constant Location
10
12
  end
11
13
  end