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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +87 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +227 -0
- data/FUNDING.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +930 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/commonmarker/merge/backend.rb +336 -0
- data/lib/commonmarker/merge/debug_logger.rb +26 -0
- data/lib/commonmarker/merge/file_analysis.rb +51 -0
- data/lib/commonmarker/merge/freeze_node.rb +32 -0
- data/lib/commonmarker/merge/smart_merger.rb +139 -0
- data/lib/commonmarker/merge/version.rb +12 -0
- data/lib/commonmarker/merge.rb +108 -0
- data/lib/commonmarker-merge.rb +4 -0
- data/sig/commonmarker/merge/conflict_resolver.rbs +48 -0
- data/sig/commonmarker/merge/debug_logger.rbs +36 -0
- data/sig/commonmarker/merge/file_aligner.rbs +27 -0
- data/sig/commonmarker/merge/file_analysis.rbs +95 -0
- data/sig/commonmarker/merge/freeze_node.rbs +65 -0
- data/sig/commonmarker/merge/merge_result.rbs +59 -0
- data/sig/commonmarker/merge/smart_merger.rbs +53 -0
- data/sig/commonmarker/merge.rbs +223 -0
- data.tar.gz.sig +0 -0
- metadata +331 -0
- metadata.gz.sig +0 -0
data/REEK
ADDED
|
File without changes
|
data/RUBOCOP.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# RuboCop Usage Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A tale of two RuboCop plugin gems.
|
|
6
|
+
|
|
7
|
+
### RuboCop Gradual
|
|
8
|
+
|
|
9
|
+
This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
|
|
10
|
+
|
|
11
|
+
### RuboCop LTS
|
|
12
|
+
|
|
13
|
+
This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
|
|
14
|
+
RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
|
|
15
|
+
|
|
16
|
+
## Checking RuboCop Violations
|
|
17
|
+
|
|
18
|
+
To check for RuboCop violations in this project, always use:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle exec rake rubocop_gradual:check
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Do not use** the standard RuboCop commands like:
|
|
25
|
+
- `bundle exec rubocop`
|
|
26
|
+
- `rubocop`
|
|
27
|
+
|
|
28
|
+
## Understanding the Lock File
|
|
29
|
+
|
|
30
|
+
The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
|
|
31
|
+
|
|
32
|
+
1. Prevent new violations while gradually fixing existing ones
|
|
33
|
+
2. Track progress on code style improvements
|
|
34
|
+
3. Ensure CI builds don't fail due to pre-existing violations
|
|
35
|
+
|
|
36
|
+
## Common Commands
|
|
37
|
+
|
|
38
|
+
- **Check violations**
|
|
39
|
+
- `bundle exec rake rubocop_gradual`
|
|
40
|
+
- `bundle exec rake rubocop_gradual:check`
|
|
41
|
+
- **(Safe) Autocorrect violations, and update lockfile if no new violations**
|
|
42
|
+
- `bundle exec rake rubocop_gradual:autocorrect`
|
|
43
|
+
- **Force update the lock file (w/o autocorrect) to match violations present in code**
|
|
44
|
+
- `bundle exec rake rubocop_gradual:force_update`
|
|
45
|
+
|
|
46
|
+
## Workflow
|
|
47
|
+
|
|
48
|
+
1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
|
|
49
|
+
a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
|
|
50
|
+
2. If there are new violations, either:
|
|
51
|
+
- Fix them in your code
|
|
52
|
+
- Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
|
|
53
|
+
3. Commit the updated `.rubocop_gradual.lock` file along with your changes
|
|
54
|
+
|
|
55
|
+
## Never add inline RuboCop disables
|
|
56
|
+
|
|
57
|
+
Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
|
|
58
|
+
|
|
59
|
+
- Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
|
|
60
|
+
- Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
|
|
61
|
+
- `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
|
|
62
|
+
- If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
|
|
63
|
+
|
|
64
|
+
In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
|
|
65
|
+
|
|
66
|
+
## Benefits of rubocop_gradual
|
|
67
|
+
|
|
68
|
+
- Allows incremental adoption of code style rules
|
|
69
|
+
- Prevents CI failures due to pre-existing violations
|
|
70
|
+
- Provides a clear record of code style debt
|
|
71
|
+
- Enables focused efforts on improving code quality over time
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|----------|-----------|
|
|
7
|
+
| 1.latest | ✅ |
|
|
8
|
+
|
|
9
|
+
## Security contact information
|
|
10
|
+
|
|
11
|
+
To report a security vulnerability, please use the
|
|
12
|
+
[Tidelift security contact](https://tidelift.com/security).
|
|
13
|
+
Tidelift will coordinate the fix and disclosure.
|
|
14
|
+
|
|
15
|
+
## Additional Support
|
|
16
|
+
|
|
17
|
+
If you are interested in support for versions older than the latest release,
|
|
18
|
+
please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
|
|
19
|
+
or find other sponsorship links in the [README].
|
|
20
|
+
|
|
21
|
+
[README]: README.md
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commonmarker
|
|
4
|
+
module Merge
|
|
5
|
+
# Commonmarker backend using the Commonmarker gem (comrak Rust parser)
|
|
6
|
+
#
|
|
7
|
+
# This backend wraps Commonmarker, a Ruby gem that provides bindings to
|
|
8
|
+
# comrak, a fast CommonMark-compliant Markdown parser written in Rust.
|
|
9
|
+
#
|
|
10
|
+
# @note This backend only parses Markdown source code
|
|
11
|
+
# @see https://github.com/gjtorikian/commonmarker Commonmarker gem
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# parser = TreeHaver::Parser.new
|
|
15
|
+
# parser.language = Commonmarker::Merge::Backend::Language.markdown
|
|
16
|
+
# tree = parser.parse(markdown_source)
|
|
17
|
+
# root = tree.root_node
|
|
18
|
+
# puts root.type # => "document"
|
|
19
|
+
module Backend
|
|
20
|
+
@load_attempted = false
|
|
21
|
+
@loaded = false
|
|
22
|
+
|
|
23
|
+
# Check if the Commonmarker backend is available
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean] true if commonmarker gem is available
|
|
26
|
+
class << self
|
|
27
|
+
def available?
|
|
28
|
+
return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
29
|
+
@load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
30
|
+
begin
|
|
31
|
+
require "commonmarker"
|
|
32
|
+
@loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
33
|
+
rescue LoadError
|
|
34
|
+
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
35
|
+
rescue StandardError
|
|
36
|
+
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
37
|
+
end
|
|
38
|
+
@loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Reset the load state (primarily for testing)
|
|
42
|
+
#
|
|
43
|
+
# @return [void]
|
|
44
|
+
# @api private
|
|
45
|
+
def reset!
|
|
46
|
+
@load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
47
|
+
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get capabilities supported by this backend
|
|
51
|
+
#
|
|
52
|
+
# @return [Hash{Symbol => Object}] capability map
|
|
53
|
+
def capabilities
|
|
54
|
+
return {} unless available?
|
|
55
|
+
{
|
|
56
|
+
backend: :commonmarker,
|
|
57
|
+
query: false,
|
|
58
|
+
bytes_field: false, # Commonmarker uses line/column
|
|
59
|
+
incremental: false,
|
|
60
|
+
pure_ruby: false, # Uses Rust via FFI
|
|
61
|
+
markdown_only: true,
|
|
62
|
+
error_tolerant: true, # Markdown is forgiving
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Commonmarker language wrapper
|
|
68
|
+
#
|
|
69
|
+
# Commonmarker only parses Markdown. This class exists for API compatibility.
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# language = Commonmarker::Merge::Backend::Language.markdown
|
|
73
|
+
# parser.language = language
|
|
74
|
+
class Language < TreeHaver::Base::Language
|
|
75
|
+
# Create a new Commonmarker language instance
|
|
76
|
+
#
|
|
77
|
+
# @param name [Symbol] Language name (should be :markdown)
|
|
78
|
+
# @param options [Hash] Commonmarker parse options
|
|
79
|
+
def initialize(name = :markdown, options: {})
|
|
80
|
+
super(name, backend: :commonmarker, options: options)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class << self
|
|
84
|
+
# Create a Markdown language instance
|
|
85
|
+
#
|
|
86
|
+
# @param options [Hash] Commonmarker parse options
|
|
87
|
+
# @return [Language] Markdown language
|
|
88
|
+
def markdown(options: {})
|
|
89
|
+
new(:markdown, options: options)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Load language from library path (API compatibility)
|
|
93
|
+
#
|
|
94
|
+
# @param _path [String] Ignored - Commonmarker doesn't load external grammars
|
|
95
|
+
# @param symbol [String, nil] Ignored
|
|
96
|
+
# @param name [String, nil] Language name hint (defaults to :markdown)
|
|
97
|
+
# @return [Language] Markdown language
|
|
98
|
+
# @raise [TreeHaver::NotAvailable] if requested language is not Markdown
|
|
99
|
+
def from_library(_path = nil, symbol: nil, name: nil)
|
|
100
|
+
lang_name = name || symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym || :markdown
|
|
101
|
+
|
|
102
|
+
unless lang_name == :markdown
|
|
103
|
+
raise TreeHaver::NotAvailable,
|
|
104
|
+
"Commonmarker backend only supports Markdown, not #{lang_name}."
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
markdown
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Commonmarker parser wrapper
|
|
113
|
+
class Parser < TreeHaver::Base::Parser
|
|
114
|
+
# Parse Markdown source code
|
|
115
|
+
#
|
|
116
|
+
# @param source [String] Markdown source to parse
|
|
117
|
+
# @return [Tree] Parsed tree
|
|
118
|
+
def parse(source)
|
|
119
|
+
raise "Language not set" unless language
|
|
120
|
+
Backend.available? or raise "Commonmarker not available"
|
|
121
|
+
|
|
122
|
+
opts = language.options || {}
|
|
123
|
+
doc = ::Commonmarker.parse(source, options: opts)
|
|
124
|
+
Tree.new(doc, source)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Commonmarker tree wrapper
|
|
129
|
+
class Tree < TreeHaver::Base::Tree
|
|
130
|
+
def initialize(document, source)
|
|
131
|
+
super(document, source: source)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def root_node
|
|
135
|
+
Node.new(inner_tree, source: source, lines: lines)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Commonmarker node wrapper
|
|
140
|
+
#
|
|
141
|
+
# Wraps Commonmarker::Node to provide TreeHaver::Node-compatible interface.
|
|
142
|
+
class Node < TreeHaver::Base::Node
|
|
143
|
+
# Get the node type as a string
|
|
144
|
+
#
|
|
145
|
+
# @return [String] Node type
|
|
146
|
+
def type
|
|
147
|
+
inner_node.type.to_s
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Alias for TreeHaver compatibility
|
|
151
|
+
alias_method :kind, :type
|
|
152
|
+
|
|
153
|
+
# Get the text content of this node
|
|
154
|
+
#
|
|
155
|
+
# @return [String] Node text
|
|
156
|
+
def text
|
|
157
|
+
if inner_node.respond_to?(:string_content)
|
|
158
|
+
begin
|
|
159
|
+
content = inner_node.string_content.to_s
|
|
160
|
+
return content unless content.empty?
|
|
161
|
+
rescue TypeError
|
|
162
|
+
# Container node - fall through
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
children.map(&:text).join
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get child nodes
|
|
169
|
+
#
|
|
170
|
+
# @return [Array<Node>] Child nodes
|
|
171
|
+
def children
|
|
172
|
+
return [] unless inner_node.respond_to?(:each)
|
|
173
|
+
|
|
174
|
+
result = []
|
|
175
|
+
inner_node.each { |child| result << Node.new(child, source: source, lines: lines) }
|
|
176
|
+
result
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Get start byte offset
|
|
180
|
+
def start_byte
|
|
181
|
+
sp = start_point
|
|
182
|
+
calculate_byte_offset(sp[:row], sp[:column])
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Get end byte offset
|
|
186
|
+
def end_byte
|
|
187
|
+
ep = end_point
|
|
188
|
+
calculate_byte_offset(ep[:row], ep[:column])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Get start point (0-based row/column)
|
|
192
|
+
# @return [Point] Start position
|
|
193
|
+
def start_point
|
|
194
|
+
if inner_node.respond_to?(:source_position)
|
|
195
|
+
begin
|
|
196
|
+
pos = inner_node.source_position
|
|
197
|
+
if pos && pos[:start_line]
|
|
198
|
+
return Point.new(pos[:start_line] - 1, (pos[:start_column] || 1) - 1)
|
|
199
|
+
end
|
|
200
|
+
rescue
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Fallback: check sourcepos (old API)
|
|
206
|
+
begin
|
|
207
|
+
pos = inner_node.sourcepos
|
|
208
|
+
return Point.new(pos[0] - 1, pos[1] - 1) if pos
|
|
209
|
+
rescue
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
Point.new(0, 0)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Get end point (0-based row/column)
|
|
217
|
+
# @return [Point] End position
|
|
218
|
+
def end_point
|
|
219
|
+
if inner_node.respond_to?(:source_position)
|
|
220
|
+
begin
|
|
221
|
+
pos = inner_node.source_position
|
|
222
|
+
if pos && pos[:end_line]
|
|
223
|
+
return Point.new(pos[:end_line] - 1, (pos[:end_column] || 1) - 1)
|
|
224
|
+
end
|
|
225
|
+
rescue
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
begin
|
|
231
|
+
pos = inner_node.sourcepos
|
|
232
|
+
return Point.new(pos[2] - 1, pos[3] - 1) if pos
|
|
233
|
+
rescue
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
Point.new(0, 0)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Commonmarker-specific methods
|
|
241
|
+
|
|
242
|
+
# Get heading level (1-6)
|
|
243
|
+
# @return [Integer, nil]
|
|
244
|
+
def header_level
|
|
245
|
+
return unless type == "heading"
|
|
246
|
+
begin
|
|
247
|
+
inner_node.header_level
|
|
248
|
+
rescue
|
|
249
|
+
nil
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Get fence info for code blocks
|
|
254
|
+
# @return [String, nil]
|
|
255
|
+
def fence_info
|
|
256
|
+
return unless type == "code_block"
|
|
257
|
+
begin
|
|
258
|
+
inner_node.fence_info
|
|
259
|
+
rescue
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Get URL for links/images
|
|
265
|
+
# @return [String, nil]
|
|
266
|
+
def url
|
|
267
|
+
inner_node.url
|
|
268
|
+
rescue
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Get title for links/images
|
|
273
|
+
# @return [String, nil]
|
|
274
|
+
def title
|
|
275
|
+
inner_node.title
|
|
276
|
+
rescue
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Get the next sibling
|
|
281
|
+
# @return [Node, nil]
|
|
282
|
+
def next_sibling
|
|
283
|
+
sibling = begin
|
|
284
|
+
inner_node.next_sibling
|
|
285
|
+
rescue
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
288
|
+
sibling ? Node.new(sibling, source: source, lines: lines) : nil
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Get the previous sibling
|
|
292
|
+
# @return [Node, nil]
|
|
293
|
+
def prev_sibling
|
|
294
|
+
sibling = begin
|
|
295
|
+
inner_node.previous_sibling
|
|
296
|
+
rescue
|
|
297
|
+
nil
|
|
298
|
+
end
|
|
299
|
+
sibling ? Node.new(sibling, source: source, lines: lines) : nil
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Get the parent node
|
|
303
|
+
# @return [Node, nil]
|
|
304
|
+
def parent
|
|
305
|
+
p = begin
|
|
306
|
+
inner_node.parent
|
|
307
|
+
rescue
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
p ? Node.new(p, source: source, lines: lines) : nil
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Alias Point to the base class for compatibility
|
|
315
|
+
Point = TreeHaver::Base::Point
|
|
316
|
+
|
|
317
|
+
# Register this backend with TreeHaver
|
|
318
|
+
# Register for generic :markdown language
|
|
319
|
+
::TreeHaver.register_language(
|
|
320
|
+
:markdown,
|
|
321
|
+
backend_type: :commonmarker,
|
|
322
|
+
backend_module: self,
|
|
323
|
+
gem_name: "commonmarker",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Register the full tag for RSpec dependency tags with require path
|
|
327
|
+
# This enables tree_haver to lazily load this gem when checking availability
|
|
328
|
+
::TreeHaver::BackendRegistry.register_tag(
|
|
329
|
+
:commonmarker_backend,
|
|
330
|
+
category: :backend,
|
|
331
|
+
backend_name: :commonmarker,
|
|
332
|
+
require_path: "commonmarker/merge",
|
|
333
|
+
) { available? }
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commonmarker
|
|
4
|
+
module Merge
|
|
5
|
+
# Debug logging utility for Commonmarker::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["COMMONMARKER_MERGE_DEBUG"] = "1"
|
|
12
|
+
# DebugLogger.debug("Parsing markdown", { file: "README.md" })
|
|
13
|
+
#
|
|
14
|
+
# @example Time an operation
|
|
15
|
+
# result = DebugLogger.time("parse") { Commonmarker.parse(source) }
|
|
16
|
+
#
|
|
17
|
+
# @see Ast::Merge::DebugLogger Base module
|
|
18
|
+
module DebugLogger
|
|
19
|
+
extend Ast::Merge::DebugLogger
|
|
20
|
+
|
|
21
|
+
# Configure for commonmarker-merge
|
|
22
|
+
self.env_var_name = "COMMONMARKER_MERGE_DEBUG"
|
|
23
|
+
self.log_prefix = "[commonmarker-merge]"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commonmarker
|
|
4
|
+
module Merge
|
|
5
|
+
# File analysis for Markdown files using CommonMarker.
|
|
6
|
+
#
|
|
7
|
+
# This is a thin wrapper around Markdown::Merge::FileAnalysis that:
|
|
8
|
+
# - Forces the :commonmarker backend
|
|
9
|
+
# - Sets the default freeze token to "commonmarker-merge"
|
|
10
|
+
# - Exposes commonmarker-specific options
|
|
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 commonmarker-merge
|
|
24
|
+
# @return [String]
|
|
25
|
+
DEFAULT_FREEZE_TOKEN = "commonmarker-merge"
|
|
26
|
+
|
|
27
|
+
# Initialize file analysis with CommonMarker backend.
|
|
28
|
+
#
|
|
29
|
+
# @param source [String] Markdown source code to analyze
|
|
30
|
+
# @param freeze_token [String] Token for freeze block markers (default: "commonmarker-merge")
|
|
31
|
+
# @param signature_generator [Proc, nil] Custom signature generator
|
|
32
|
+
# @param options [Hash] CommonMarker parse options
|
|
33
|
+
def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, options: {})
|
|
34
|
+
super(
|
|
35
|
+
source,
|
|
36
|
+
backend: :commonmarker,
|
|
37
|
+
freeze_token: freeze_token,
|
|
38
|
+
signature_generator: signature_generator,
|
|
39
|
+
options: options,
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the FreezeNode class to use.
|
|
44
|
+
#
|
|
45
|
+
# @return [Class] Commonmarker::Merge::FreezeNode
|
|
46
|
+
def freeze_node_class
|
|
47
|
+
FreezeNode
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commonmarker
|
|
4
|
+
module Merge
|
|
5
|
+
# Represents a frozen block of Markdown content that should be preserved during merges.
|
|
6
|
+
#
|
|
7
|
+
# Inherits from Markdown::Merge::FreezeNode which provides the generic
|
|
8
|
+
# freeze block handling.
|
|
9
|
+
#
|
|
10
|
+
# Freeze blocks are marked with HTML comments:
|
|
11
|
+
# <!-- commonmarker-merge:freeze -->
|
|
12
|
+
# ... frozen content ...
|
|
13
|
+
# <!-- commonmarker-merge:unfreeze -->
|
|
14
|
+
#
|
|
15
|
+
# @example Basic freeze block
|
|
16
|
+
# <!-- commonmarker-merge:freeze -->
|
|
17
|
+
# ## Custom Section
|
|
18
|
+
# This content will not be modified by merge operations.
|
|
19
|
+
# <!-- commonmarker-merge:unfreeze -->
|
|
20
|
+
#
|
|
21
|
+
# @example Freeze block with reason
|
|
22
|
+
# <!-- commonmarker-merge:freeze Manual TOC -->
|
|
23
|
+
# ## Table of Contents
|
|
24
|
+
# - [Introduction](#introduction)
|
|
25
|
+
# - [Usage](#usage)
|
|
26
|
+
# <!-- commonmarker-merge:unfreeze -->
|
|
27
|
+
#
|
|
28
|
+
# @see Markdown::Merge::FreezeNode
|
|
29
|
+
class FreezeNode < Markdown::Merge::FreezeNode
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commonmarker
|
|
4
|
+
module Merge
|
|
5
|
+
# Orchestrates the smart merge process for Markdown files using CommonMarker.
|
|
6
|
+
#
|
|
7
|
+
# This is a thin wrapper around Markdown::Merge::SmartMerger that:
|
|
8
|
+
# - Forces the :commonmarker backend
|
|
9
|
+
# - Sets commonmarker-specific defaults (freeze token, inner_merge_code_blocks)
|
|
10
|
+
# - Exposes commonmarker-specific options (options hash)
|
|
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
|
+
# @see Markdown::Merge::SmartMerger Underlying implementation
|
|
44
|
+
class SmartMerger < Markdown::Merge::SmartMerger
|
|
45
|
+
# Creates a new SmartMerger for intelligent Markdown file merging.
|
|
46
|
+
#
|
|
47
|
+
# @param template_content [String] Template Markdown source code
|
|
48
|
+
# @param dest_content [String] Destination Markdown source code
|
|
49
|
+
#
|
|
50
|
+
# @param signature_generator [Proc, nil] Optional proc to generate custom node signatures.
|
|
51
|
+
# The proc receives a node (wrapped with canonical merge_type) and should return one of:
|
|
52
|
+
# - An array representing the node's signature
|
|
53
|
+
# - `nil` to indicate the node should have no signature
|
|
54
|
+
# - The original node to fall through to default signature computation
|
|
55
|
+
#
|
|
56
|
+
# @param preference [Symbol] Controls which version to use when nodes
|
|
57
|
+
# have matching signatures but different content:
|
|
58
|
+
# - `:destination` (default) - Use destination version (preserves customizations)
|
|
59
|
+
# - `:template` - Use template version (applies updates)
|
|
60
|
+
#
|
|
61
|
+
# @param add_template_only_nodes [Boolean] Controls whether to add nodes that only
|
|
62
|
+
# exist in template:
|
|
63
|
+
# - `false` (default) - Skip template-only nodes
|
|
64
|
+
# - `true` - Add template-only nodes to result
|
|
65
|
+
#
|
|
66
|
+
# @param freeze_token [String] Token to use for freeze block markers.
|
|
67
|
+
# Default: "commonmarker-merge"
|
|
68
|
+
# Looks for: <!-- commonmarker-merge:freeze --> / <!-- commonmarker-merge:unfreeze -->
|
|
69
|
+
#
|
|
70
|
+
# @param options [Hash] CommonMarker parse options
|
|
71
|
+
#
|
|
72
|
+
# @param match_refiner [#call, nil] Optional match refiner for fuzzy matching of
|
|
73
|
+
# unmatched nodes. Default: nil (fuzzy matching disabled).
|
|
74
|
+
# Set to TableMatchRefiner.new to enable fuzzy table matching.
|
|
75
|
+
#
|
|
76
|
+
# @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
77
|
+
# for per-node-type merge preferences.
|
|
78
|
+
# @param extra_options [Hash] Additional options for forward compatibility
|
|
79
|
+
#
|
|
80
|
+
# @raise [TemplateParseError] If template has syntax errors
|
|
81
|
+
# @raise [DestinationParseError] If destination has syntax errors
|
|
82
|
+
def initialize(
|
|
83
|
+
template_content,
|
|
84
|
+
dest_content,
|
|
85
|
+
signature_generator: nil,
|
|
86
|
+
preference: :destination,
|
|
87
|
+
add_template_only_nodes: false,
|
|
88
|
+
freeze_token: DEFAULT_FREEZE_TOKEN,
|
|
89
|
+
options: {},
|
|
90
|
+
match_refiner: nil,
|
|
91
|
+
node_typing: nil,
|
|
92
|
+
**extra_options
|
|
93
|
+
)
|
|
94
|
+
super(
|
|
95
|
+
template_content,
|
|
96
|
+
dest_content,
|
|
97
|
+
backend: :commonmarker,
|
|
98
|
+
signature_generator: signature_generator,
|
|
99
|
+
preference: preference,
|
|
100
|
+
add_template_only_nodes: add_template_only_nodes,
|
|
101
|
+
inner_merge_code_blocks: DEFAULT_INNER_MERGE_CODE_BLOCKS,
|
|
102
|
+
freeze_token: freeze_token,
|
|
103
|
+
match_refiner: match_refiner,
|
|
104
|
+
node_typing: node_typing,
|
|
105
|
+
options: options,
|
|
106
|
+
**extra_options
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns the TemplateParseError class to use.
|
|
111
|
+
#
|
|
112
|
+
# @return [Class] Commonmarker::Merge::TemplateParseError
|
|
113
|
+
def template_parse_error_class
|
|
114
|
+
TemplateParseError
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns the DestinationParseError class to use.
|
|
118
|
+
#
|
|
119
|
+
# @return [Class] Commonmarker::Merge::DestinationParseError
|
|
120
|
+
def destination_parse_error_class
|
|
121
|
+
DestinationParseError
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Create a FileAnalysis instance for parsing.
|
|
125
|
+
#
|
|
126
|
+
# @param content [String] Markdown content to analyze
|
|
127
|
+
# @param options [Hash] Analysis options
|
|
128
|
+
# @return [Commonmarker::Merge::FileAnalysis] File analysis instance
|
|
129
|
+
def create_file_analysis(content, **opts)
|
|
130
|
+
FileAnalysis.new(
|
|
131
|
+
content,
|
|
132
|
+
freeze_token: opts[:freeze_token],
|
|
133
|
+
signature_generator: opts[:signature_generator],
|
|
134
|
+
options: opts[:options] || {},
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commonmarker
|
|
4
|
+
module Merge
|
|
5
|
+
# Version information for Commonmarker::Merge
|
|
6
|
+
module Version
|
|
7
|
+
# Current version of the commonmarker-merge gem
|
|
8
|
+
VERSION = "1.0.0"
|
|
9
|
+
end
|
|
10
|
+
VERSION = Version::VERSION # traditional location
|
|
11
|
+
end
|
|
12
|
+
end
|