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.
- 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 +815 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/markly/merge/backend.rb +422 -0
- data/lib/markly/merge/debug_logger.rb +26 -0
- data/lib/markly/merge/file_analysis.rb +53 -0
- data/lib/markly/merge/freeze_node.rb +32 -0
- data/lib/markly/merge/smart_merger.rb +159 -0
- data/lib/markly/merge/version.rb +12 -0
- data/lib/markly/merge.rb +110 -0
- data/lib/markly-merge.rb +6 -0
- data/sig/markly/merge.rbs +106 -0
- data.tar.gz.sig +0 -0
- metadata +368 -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,422 @@
|
|
|
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
|
+
@load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
24
|
+
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
25
|
+
|
|
26
|
+
# Check if the Markly backend is available
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true if markly gem is available
|
|
29
|
+
class << self
|
|
30
|
+
def available?
|
|
31
|
+
return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
32
|
+
@load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
33
|
+
begin
|
|
34
|
+
require "markly"
|
|
35
|
+
@loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
36
|
+
rescue LoadError
|
|
37
|
+
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
38
|
+
rescue StandardError
|
|
39
|
+
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
40
|
+
end
|
|
41
|
+
@loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Reset the load state (primarily for testing)
|
|
45
|
+
#
|
|
46
|
+
# @return [void]
|
|
47
|
+
# @api private
|
|
48
|
+
def reset!
|
|
49
|
+
@load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
50
|
+
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get capabilities supported by this backend
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash{Symbol => Object}] capability map
|
|
56
|
+
def capabilities
|
|
57
|
+
return {} unless available?
|
|
58
|
+
{
|
|
59
|
+
backend: :markly,
|
|
60
|
+
query: false,
|
|
61
|
+
bytes_field: false, # Markly uses line/column
|
|
62
|
+
incremental: false,
|
|
63
|
+
pure_ruby: false, # Uses C via FFI
|
|
64
|
+
markdown_only: true,
|
|
65
|
+
error_tolerant: true, # Markdown is forgiving
|
|
66
|
+
gfm_extensions: true, # Supports GitHub Flavored Markdown
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Markly language wrapper
|
|
72
|
+
#
|
|
73
|
+
# Markly only parses Markdown. This class exists for API compatibility
|
|
74
|
+
# and to pass through Markly-specific options (flags, extensions).
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# language = Markly::Merge::Backend::Language.markdown(
|
|
78
|
+
# flags: Markly::DEFAULT | Markly::FOOTNOTES,
|
|
79
|
+
# extensions: [:table, :strikethrough]
|
|
80
|
+
# )
|
|
81
|
+
# parser.language = language
|
|
82
|
+
class Language < ::TreeHaver::Base::Language
|
|
83
|
+
# Markly parse flags
|
|
84
|
+
# @return [Integer]
|
|
85
|
+
attr_reader :flags
|
|
86
|
+
|
|
87
|
+
# Markly extensions to enable
|
|
88
|
+
# @return [Array<Symbol>]
|
|
89
|
+
attr_reader :extensions
|
|
90
|
+
|
|
91
|
+
# Create a new Markly language instance
|
|
92
|
+
#
|
|
93
|
+
# @param name [Symbol] Language name (should be :markdown)
|
|
94
|
+
# @param flags [Integer] Markly parse flags (default: Markly::DEFAULT)
|
|
95
|
+
# @param extensions [Array<Symbol>] Extensions to enable (default: [:table])
|
|
96
|
+
# @param options [Hash] parsing options (reserved for future use)
|
|
97
|
+
def initialize(name = :markdown, flags: nil, extensions: [:table], options: {})
|
|
98
|
+
super(name, backend: :markly, options: options.merge({flags: flags, extensions: extensions}))
|
|
99
|
+
@flags = flags # Will use Markly::DEFAULT if nil at parse time
|
|
100
|
+
@extensions = extensions
|
|
101
|
+
|
|
102
|
+
unless @name == :markdown
|
|
103
|
+
raise TreeHaver::NotAvailable,
|
|
104
|
+
"Markly backend only supports Markdown parsing. " \
|
|
105
|
+
"Got language: #{name.inspect}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class << self
|
|
110
|
+
# Create a Markdown language instance
|
|
111
|
+
#
|
|
112
|
+
# @param flags [Integer] Markly parse flags
|
|
113
|
+
# @param extensions [Array<Symbol>] Extensions to enable
|
|
114
|
+
# @param options [Hash] parsing options (reserved for future use)
|
|
115
|
+
# @return [Language] Markdown language
|
|
116
|
+
def markdown(flags: nil, extensions: [:table], options: {})
|
|
117
|
+
new(:markdown, flags: flags, extensions: extensions, options: options)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Load language from library path (API compatibility)
|
|
121
|
+
#
|
|
122
|
+
# @param _path [String] Ignored - Markly doesn't load external grammars
|
|
123
|
+
# @param symbol [String, nil] Ignored
|
|
124
|
+
# @param name [String, nil] Language name hint (defaults to :markdown)
|
|
125
|
+
# @return [Language] Markdown language
|
|
126
|
+
# @raise [TreeHaver::NotAvailable] if requested language is not Markdown
|
|
127
|
+
def from_library(_path = nil, symbol: nil, name: nil)
|
|
128
|
+
lang_name = name || symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym || :markdown
|
|
129
|
+
|
|
130
|
+
unless lang_name == :markdown
|
|
131
|
+
raise TreeHaver::NotAvailable,
|
|
132
|
+
"Markly backend only supports Markdown, not #{lang_name}. " \
|
|
133
|
+
"Use a tree-sitter backend for #{lang_name} support."
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
markdown
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Markly parser wrapper
|
|
142
|
+
class Parser < ::TreeHaver::Base::Parser
|
|
143
|
+
# Create a new RBS parser instance
|
|
144
|
+
#
|
|
145
|
+
# @raise [TreeHaver::NotAvailable] if rbs gem is not available
|
|
146
|
+
def initialize
|
|
147
|
+
super()
|
|
148
|
+
raise TreeHaver::NotAvailable, "markly gem not available" unless Backend.available?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Set the language for this parser
|
|
152
|
+
#
|
|
153
|
+
# @param lang [Language, Symbol] RBS language (should be :rbs or Language instance)
|
|
154
|
+
# @return [void]
|
|
155
|
+
def language=(lang)
|
|
156
|
+
case lang
|
|
157
|
+
when Language
|
|
158
|
+
@language = lang
|
|
159
|
+
when Symbol, String
|
|
160
|
+
if lang.to_sym == :markdown
|
|
161
|
+
@language = Language.markdown
|
|
162
|
+
else
|
|
163
|
+
raise ArgumentError,
|
|
164
|
+
"Markly backend only supports Markdown parsing. Got: #{lang.inspect}"
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
raise ArgumentError,
|
|
168
|
+
"Expected Backend::Language or :markdown, got #{lang.class}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Parse Markdown source code
|
|
173
|
+
#
|
|
174
|
+
# @param source [String] Markdown source to parse
|
|
175
|
+
# @return [Tree] Parsed tree
|
|
176
|
+
def parse(source)
|
|
177
|
+
raise "Language not set" unless language
|
|
178
|
+
Backend.available? or raise "Markly not available"
|
|
179
|
+
|
|
180
|
+
flags = language.flags || ::Markly::DEFAULT
|
|
181
|
+
exts = language.extensions || [:table]
|
|
182
|
+
doc = ::Markly.parse(source, flags: flags, extensions: exts)
|
|
183
|
+
Tree.new(doc, source)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Markly tree wrapper
|
|
188
|
+
#
|
|
189
|
+
# Wraps Markly parse results to provide tree-sitter-compatible API.
|
|
190
|
+
#
|
|
191
|
+
# @api private
|
|
192
|
+
class Tree < ::TreeHaver::Base::Tree
|
|
193
|
+
def initialize(document, source)
|
|
194
|
+
super(document, source: source)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def root_node
|
|
198
|
+
Node.new(inner_tree, source: source, lines: lines)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Markly node wrapper
|
|
203
|
+
#
|
|
204
|
+
# Wraps Markly::Node to provide TreeHaver::Node-compatible interface.
|
|
205
|
+
class Node < ::TreeHaver::Base::Node
|
|
206
|
+
# Type normalization map (Markly → canonical)
|
|
207
|
+
TYPE_MAP = {
|
|
208
|
+
header: "heading",
|
|
209
|
+
hrule: "thematic_break",
|
|
210
|
+
html: "html_block",
|
|
211
|
+
}.freeze
|
|
212
|
+
|
|
213
|
+
# Default source position for nodes that don't have position info
|
|
214
|
+
DEFAULT_SOURCE_POSITION = {
|
|
215
|
+
start_line: 1,
|
|
216
|
+
start_column: 1,
|
|
217
|
+
end_line: 1,
|
|
218
|
+
end_column: 1,
|
|
219
|
+
}.freeze
|
|
220
|
+
|
|
221
|
+
# Get source position from the inner Markly node
|
|
222
|
+
#
|
|
223
|
+
# @return [Hash{Symbol => Integer}] Source position from Markly
|
|
224
|
+
# @api private
|
|
225
|
+
def inner_source_position
|
|
226
|
+
@inner_source_position ||= if inner_node.respond_to?(:source_position)
|
|
227
|
+
inner_node.source_position || DEFAULT_SOURCE_POSITION
|
|
228
|
+
else
|
|
229
|
+
DEFAULT_SOURCE_POSITION
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Get the node type as a string (normalized)
|
|
234
|
+
#
|
|
235
|
+
# @return [String] Node type
|
|
236
|
+
def type
|
|
237
|
+
raw = inner_node.type.to_s
|
|
238
|
+
TYPE_MAP[raw.to_sym]&.to_s || raw
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Get the raw (non-normalized) type
|
|
242
|
+
# @return [String]
|
|
243
|
+
def raw_type
|
|
244
|
+
inner_node.type.to_s
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Get the text content of this node
|
|
248
|
+
#
|
|
249
|
+
# @return [String] Node text
|
|
250
|
+
def text
|
|
251
|
+
if inner_node.respond_to?(:string_content)
|
|
252
|
+
content = inner_node.string_content.to_s
|
|
253
|
+
return content unless content.empty?
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
if inner_node.respond_to?(:to_plaintext)
|
|
257
|
+
begin
|
|
258
|
+
inner_node.to_plaintext
|
|
259
|
+
rescue
|
|
260
|
+
children.map(&:text).join
|
|
261
|
+
end
|
|
262
|
+
else
|
|
263
|
+
children.map(&:text).join
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Get child nodes (Markly uses first_child/next pattern)
|
|
268
|
+
#
|
|
269
|
+
# @return [Array<Node>] Child nodes
|
|
270
|
+
def children
|
|
271
|
+
result = []
|
|
272
|
+
child = begin
|
|
273
|
+
inner_node.first_child
|
|
274
|
+
rescue
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
while child
|
|
278
|
+
result << Node.new(child, source: source, lines: lines)
|
|
279
|
+
child = begin
|
|
280
|
+
child.next
|
|
281
|
+
rescue
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
result
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Position information
|
|
289
|
+
|
|
290
|
+
def start_byte
|
|
291
|
+
pos = inner_source_position
|
|
292
|
+
calculate_byte_offset(pos[:start_line] - 1, pos[:start_column] - 1)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def end_byte
|
|
296
|
+
pos = inner_source_position
|
|
297
|
+
calculate_byte_offset(pos[:end_line] - 1, pos[:end_column] - 1)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def start_point
|
|
301
|
+
pos = inner_source_position
|
|
302
|
+
{row: pos[:start_line] - 1, column: pos[:start_column] - 1}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def end_point
|
|
306
|
+
pos = inner_source_position
|
|
307
|
+
{row: pos[:end_line] - 1, column: pos[:end_column] - 1}
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Convert node to CommonMark/Markdown/HTML/plaintext
|
|
311
|
+
def to_commonmark
|
|
312
|
+
inner_node.to_commonmark
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def to_markdown
|
|
316
|
+
inner_node.to_markdown
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def to_plaintext
|
|
320
|
+
inner_node.to_plaintext
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def to_html
|
|
324
|
+
inner_node.to_html
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Markly-specific methods
|
|
328
|
+
|
|
329
|
+
# Get heading level (1-6)
|
|
330
|
+
# @return [Integer, nil]
|
|
331
|
+
def header_level
|
|
332
|
+
return unless raw_type == "header"
|
|
333
|
+
begin
|
|
334
|
+
inner_node.header_level
|
|
335
|
+
rescue
|
|
336
|
+
nil
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Get fence info for code blocks
|
|
341
|
+
# @return [String, nil]
|
|
342
|
+
def fence_info
|
|
343
|
+
return unless type == "code_block"
|
|
344
|
+
begin
|
|
345
|
+
inner_node.fence_info
|
|
346
|
+
rescue
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Get URL for links/images
|
|
352
|
+
# @return [String, nil]
|
|
353
|
+
def url
|
|
354
|
+
inner_node.url
|
|
355
|
+
rescue
|
|
356
|
+
nil
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Get title for links/images
|
|
360
|
+
# @return [String, nil]
|
|
361
|
+
def title
|
|
362
|
+
inner_node.title
|
|
363
|
+
rescue
|
|
364
|
+
nil
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Get the next sibling (Markly uses .next)
|
|
368
|
+
# @return [Node, nil]
|
|
369
|
+
def next_sibling
|
|
370
|
+
sibling = begin
|
|
371
|
+
inner_node.next
|
|
372
|
+
rescue
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
sibling ? Node.new(sibling, source: source, lines: lines) : nil
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Get the previous sibling
|
|
379
|
+
# @return [Node, nil]
|
|
380
|
+
def prev_sibling
|
|
381
|
+
sibling = begin
|
|
382
|
+
inner_node.previous
|
|
383
|
+
rescue
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
386
|
+
sibling ? Node.new(sibling, source: source, lines: lines) : nil
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Get the parent node
|
|
390
|
+
# @return [Node, nil]
|
|
391
|
+
def parent
|
|
392
|
+
p = begin
|
|
393
|
+
inner_node.parent
|
|
394
|
+
rescue
|
|
395
|
+
nil
|
|
396
|
+
end
|
|
397
|
+
p ? Node.new(p, source: source, lines: lines) : nil
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Alias Point to the base class for compatibility
|
|
402
|
+
Point = ::TreeHaver::Base::Point
|
|
403
|
+
|
|
404
|
+
# Register this backend with TreeHaver
|
|
405
|
+
::TreeHaver.register_language(
|
|
406
|
+
:markdown,
|
|
407
|
+
backend_type: :markly,
|
|
408
|
+
backend_module: self,
|
|
409
|
+
gem_name: "markly",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Register the full tag for RSpec dependency tags with require path
|
|
413
|
+
# This enables tree_haver to lazily load this gem when checking availability
|
|
414
|
+
::TreeHaver::BackendRegistry.register_tag(
|
|
415
|
+
:markly_backend,
|
|
416
|
+
category: :backend,
|
|
417
|
+
backend_name: :markly,
|
|
418
|
+
require_path: "markly/merge",
|
|
419
|
+
) { available? }
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
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
|
+
extend Ast::Merge::DebugLogger
|
|
20
|
+
|
|
21
|
+
# Configure for markly-merge
|
|
22
|
+
self.env_var_name = "MARKLY_MERGE_DEBUG"
|
|
23
|
+
self.log_prefix = "[markly-merge]"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
# Initialize file analysis with Markly backend.
|
|
28
|
+
#
|
|
29
|
+
# @param source [String] Markdown source code to analyze
|
|
30
|
+
# @param freeze_token [String] Token for freeze block markers (default: "markly-merge")
|
|
31
|
+
# @param signature_generator [Proc, nil] Custom signature generator
|
|
32
|
+
# @param flags [Integer] Markly parse flags (e.g., Markly::FOOTNOTES | Markly::SMART)
|
|
33
|
+
# @param extensions [Array<Symbol>] Markly extensions to enable (e.g., [:table, :strikethrough])
|
|
34
|
+
def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, flags: ::Markly::DEFAULT, extensions: [:table])
|
|
35
|
+
super(
|
|
36
|
+
source,
|
|
37
|
+
backend: :markly,
|
|
38
|
+
freeze_token: freeze_token,
|
|
39
|
+
signature_generator: signature_generator,
|
|
40
|
+
flags: flags,
|
|
41
|
+
extensions: extensions,
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns the FreezeNode class to use.
|
|
46
|
+
#
|
|
47
|
+
# @return [Class] Markly::Merge::FreezeNode
|
|
48
|
+
def freeze_node_class
|
|
49
|
+
FreezeNode
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markly
|
|
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
|
+
# <!-- markly-merge:freeze -->
|
|
12
|
+
# ... frozen content ...
|
|
13
|
+
# <!-- markly-merge:unfreeze -->
|
|
14
|
+
#
|
|
15
|
+
# @example Basic freeze block
|
|
16
|
+
# <!-- markly-merge:freeze -->
|
|
17
|
+
# ## Custom Section
|
|
18
|
+
# This content will not be modified by merge operations.
|
|
19
|
+
# <!-- markly-merge:unfreeze -->
|
|
20
|
+
#
|
|
21
|
+
# @example Freeze block with reason
|
|
22
|
+
# <!-- markly-merge:freeze Manual TOC -->
|
|
23
|
+
# ## Table of Contents
|
|
24
|
+
# - [Introduction](#introduction)
|
|
25
|
+
# - [Usage](#usage)
|
|
26
|
+
# <!-- markly-merge:unfreeze -->
|
|
27
|
+
#
|
|
28
|
+
# @see Markdown::Merge::FreezeNode
|
|
29
|
+
class FreezeNode < Markdown::Merge::FreezeNode
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|