ast-merge 2.0.10 → 3.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +47 -1
- data/README.md +1 -1
- data/lib/ast/merge/content_match_refiner.rb +6 -17
- data/lib/ast/merge/file_analyzable.rb +4 -0
- data/lib/ast/merge/navigable_statement.rb +3 -11
- data/lib/ast/merge/node_typing.rb +4 -1
- data/lib/ast/merge/{partial_template_merger.rb → partial_template_merger_base.rb} +70 -165
- data/lib/ast/merge/recipe/preset.rb +13 -0
- data/lib/ast/merge/recipe/runner.rb +23 -3
- data/lib/ast/merge/version.rb +1 -1
- data/lib/ast/merge.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +7 -7
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79e0b486476d9494e6e1065d71b13ef8b940bd7d5db8caffd4784231ce239ad2
|
|
4
|
+
data.tar.gz: 941f9604854c537b087b76a253945f35dbc81f8f5b701f60dcad0ab1b9a35026
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8b51479340dd11352faac1e7ca2933481dcbf012b1fbff8176a5f425e07717848dce99f4b631cdc5562fc897e4ae2bb7af06d9380d105a6f25ab612473cd231
|
|
7
|
+
data.tar.gz: d45e550372da739a549ba78c27fe15836a8ca17b674c88dcee958f0f58732f54b7d52d6d9b99a22ee635f2ad1862c9a2631e8cc15fb4fa3c38aca528d58142cc
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,50 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [3.0.0] - 2026-01-05
|
|
34
|
+
|
|
35
|
+
- TAG: [v3.0.0][3.0.0t]
|
|
36
|
+
- COVERAGE: 96.93% -- 2462/2540 lines in 47 files
|
|
37
|
+
- BRANCH COVERAGE: 89.62% -- 794/886 branches in 47 files
|
|
38
|
+
- 98.72% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `TestableNode` spec helper class that wraps a mock in a real `TreeHaver::Node`, providing consistent API testing without relying on fragile mocks
|
|
43
|
+
- `Recipe::Preset#match_refiner` accessor method (was missing, causing errors in Recipe::Runner)
|
|
44
|
+
- Minimal reproduction specs for `to_commonmark` normalization behavior:
|
|
45
|
+
- `spec/integration/link_reference_preservation_spec.rb` - tests link ref preservation
|
|
46
|
+
- `spec/integration/table_formatting_preservation_spec.rb` - tests table padding preservation
|
|
47
|
+
- `Ast::Merge::PartialTemplateMergerBase` - Abstract base class for parser-agnostic partial template merging
|
|
48
|
+
- `#build_position_based_signature_generator` - Creates signature generators that match elements by position
|
|
49
|
+
- Position counters reset per document key, enabling tables at same position to match regardless of structure
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- **BREAKING**: `NavigableStatement#text` now requires nodes to conform to TreeHaver Node API (must have `#text` method)
|
|
54
|
+
- Removed conditional fallbacks for `to_plaintext`, `to_commonmark`, `slice`
|
|
55
|
+
- Nodes must now implement `#text` directly (all TreeHaver backends already do)
|
|
56
|
+
- **BREAKING**: `ContentMatchRefiner#extract_content` now requires nodes to conform to TreeHaver Node API
|
|
57
|
+
- Removed conditional fallbacks for `text_content`, `string_content`, `content`, `to_s`
|
|
58
|
+
- Custom `content_extractor` proc still supported for non-standard nodes
|
|
59
|
+
- Signature generators and typing scripts now receive TreeHaver nodes directly (no NavigableStatement wrapping)
|
|
60
|
+
- Removed NavigableStatement wrapping from `FileAnalyzable#generate_signature` and `NodeTyping.process`
|
|
61
|
+
|
|
62
|
+
### Removed
|
|
63
|
+
|
|
64
|
+
- **BREAKING**: `Ast::Merge::PartialTemplateMerger` removed. Use `Markdown::Merge::PartialTemplateMerger` directly.
|
|
65
|
+
- The base class `Ast::Merge::PartialTemplateMergerBase` remains for other parsers to extend
|
|
66
|
+
- Migration: change `Ast::Merge::PartialTemplateMerger.new(parser: :markly, ...)` to
|
|
67
|
+
`Markdown::Merge::PartialTemplateMerger.new(backend: :markly, ...)`
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Source-based rendering**: `Markdown::Merge::PartialTemplateMerger#node_to_text` now prefers extracting
|
|
72
|
+
original source text using `analysis.source_range` instead of `to_commonmark`. This preserves:
|
|
73
|
+
- Link reference definitions (no conversion to inline links)
|
|
74
|
+
- Table column padding/alignment
|
|
75
|
+
- Original formatting exactly as written
|
|
76
|
+
|
|
33
77
|
## [2.0.10] - 2026-01-04
|
|
34
78
|
|
|
35
79
|
- TAG: [v2.0.10][2.0.10t]
|
|
@@ -463,7 +507,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
463
507
|
|
|
464
508
|
- Initial release
|
|
465
509
|
|
|
466
|
-
[Unreleased]: https://github.com/kettle-rb/ast-merge/compare/
|
|
510
|
+
[Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v3.0.0...HEAD
|
|
511
|
+
[3.0.0]: https://github.com/kettle-rb/ast-merge/compare/v2.0.10...v3.0.0
|
|
512
|
+
[3.0.0t]: https://github.com/kettle-rb/ast-merge/releases/tag/v3.0.0
|
|
467
513
|
[2.0.10]: https://github.com/kettle-rb/ast-merge/compare/v2.0.9...v2.0.10
|
|
468
514
|
[2.0.10t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.10
|
|
469
515
|
[2.0.9]: https://github.com/kettle-rb/ast-merge/compare/v2.0.8...v2.0.9
|
data/README.md
CHANGED
|
@@ -1033,7 +1033,7 @@ Thanks for RTFM. ☺️
|
|
|
1033
1033
|
[📌gitmoji]: https://gitmoji.dev
|
|
1034
1034
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1035
1035
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1036
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-2.
|
|
1036
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-2.540-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1037
1037
|
[🔐security]: SECURITY.md
|
|
1038
1038
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1039
1039
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -140,28 +140,17 @@ module Ast
|
|
|
140
140
|
|
|
141
141
|
# Extract text content from a node.
|
|
142
142
|
#
|
|
143
|
-
# Uses the custom content_extractor if provided, otherwise
|
|
144
|
-
#
|
|
143
|
+
# Uses the custom content_extractor if provided, otherwise uses the
|
|
144
|
+
# standard #text method that all TreeHaver nodes provide.
|
|
145
145
|
#
|
|
146
|
-
# @param node [Object] The node
|
|
146
|
+
# @param node [Object] The node (must conform to TreeHaver Node API)
|
|
147
147
|
# @return [String] The text content
|
|
148
148
|
def extract_content(node)
|
|
149
149
|
return @content_extractor.call(node) if @content_extractor
|
|
150
150
|
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
elsif node.respond_to?(:string_content)
|
|
155
|
-
node.string_content.to_s
|
|
156
|
-
elsif node.respond_to?(:content)
|
|
157
|
-
node.content.to_s
|
|
158
|
-
elsif node.respond_to?(:text)
|
|
159
|
-
node.text.to_s
|
|
160
|
-
elsif node.respond_to?(:to_s)
|
|
161
|
-
node.to_s
|
|
162
|
-
else
|
|
163
|
-
""
|
|
164
|
-
end
|
|
151
|
+
# TreeHaver nodes (and any node conforming to the unified API) provide #text.
|
|
152
|
+
# No conditional fallbacks - nodes must conform to the API.
|
|
153
|
+
node.text.to_s
|
|
165
154
|
end
|
|
166
155
|
|
|
167
156
|
# Compute similarity score between two nodes based on content.
|
|
@@ -224,6 +224,10 @@ module Ast
|
|
|
224
224
|
# - Type checks work (e.g., `node.is_a?(Prism::CallNode)`)
|
|
225
225
|
# - The generator sees the real AST structure
|
|
226
226
|
# - Frozen nodes match by their underlying identity
|
|
227
|
+
#
|
|
228
|
+
# NOTE: For TreeHaver-based backends, the node already has a unified API
|
|
229
|
+
# with #text, #type, #source_position methods. For other backends, they
|
|
230
|
+
# must conform to the same API (either via TreeHaver or equivalent adapter).
|
|
227
231
|
custom_result = signature_generator.call(actual_node)
|
|
228
232
|
case custom_result
|
|
229
233
|
when Array, nil
|
|
@@ -270,17 +270,9 @@ module Ast
|
|
|
270
270
|
|
|
271
271
|
# @return [String] Node text content
|
|
272
272
|
def text
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
node.to_commonmark.to_s
|
|
277
|
-
elsif node.respond_to?(:slice)
|
|
278
|
-
node.slice.to_s
|
|
279
|
-
elsif node.respond_to?(:text)
|
|
280
|
-
node.text.to_s
|
|
281
|
-
else
|
|
282
|
-
node.to_s
|
|
283
|
-
end
|
|
273
|
+
# TreeHaver nodes (and any node conforming to the unified API) provide #text.
|
|
274
|
+
# No conditional fallbacks - nodes must conform to the API.
|
|
275
|
+
node.text.to_s
|
|
284
276
|
end
|
|
285
277
|
|
|
286
278
|
# @return [Hash, nil] Source position info
|
|
@@ -139,7 +139,10 @@ module Ast
|
|
|
139
139
|
callable = find_typing_callable(typing_config, type_key, node)
|
|
140
140
|
return node unless callable
|
|
141
141
|
|
|
142
|
-
# Call the typing callable with the node
|
|
142
|
+
# Call the typing callable with the node.
|
|
143
|
+
# NOTE: For TreeHaver-based backends, the node already has a unified API
|
|
144
|
+
# with #text, #type, #source_position methods. For other backends, they
|
|
145
|
+
# must conform to the same API (either via TreeHaver or equivalent adapter).
|
|
143
146
|
callable.call(node)
|
|
144
147
|
end
|
|
145
148
|
|
|
@@ -2,36 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module Ast
|
|
4
4
|
module Merge
|
|
5
|
-
#
|
|
5
|
+
# Base class for merging a partial template into a specific section of a destination document.
|
|
6
6
|
#
|
|
7
|
-
# Unlike the full SmartMerger which merges entire documents,
|
|
7
|
+
# Unlike the full SmartMerger which merges entire documents, PartialTemplateMergerBase:
|
|
8
8
|
# 1. Finds a specific section in the destination (using InjectionPoint)
|
|
9
9
|
# 2. Replaces/merges only that section with the template
|
|
10
10
|
# 3. Leaves the rest of the destination unchanged
|
|
11
11
|
#
|
|
12
|
-
# This is
|
|
13
|
-
#
|
|
12
|
+
# This is an abstract base class. Subclasses must implement:
|
|
13
|
+
# - #create_analysis(content) - Create a FileAnalysis for the given content
|
|
14
|
+
# - #create_smart_merger(template, section) - Create a SmartMerger for the section merge
|
|
15
|
+
# - #find_section_end(statements, injection_point) - Find where the section ends
|
|
16
|
+
# - #node_to_text(node, analysis) - Convert a node to source text
|
|
14
17
|
#
|
|
15
|
-
# @
|
|
16
|
-
#
|
|
17
|
-
# template: template_content,
|
|
18
|
-
# destination: destination_content,
|
|
19
|
-
# anchor: { type: :heading, text: /Gem Family/ },
|
|
20
|
-
# parser: :markly
|
|
21
|
-
# )
|
|
22
|
-
# result = merger.merge
|
|
23
|
-
# puts result.content
|
|
18
|
+
# @abstract Subclass and implement parser-specific methods
|
|
19
|
+
# @see Markdown::Merge::PartialTemplateMerger For markdown implementation
|
|
24
20
|
#
|
|
25
|
-
|
|
26
|
-
# merger = PartialTemplateMerger.new(
|
|
27
|
-
# template: template_content,
|
|
28
|
-
# destination: destination_content,
|
|
29
|
-
# anchor: { type: :heading, text: /Installation/ },
|
|
30
|
-
# boundary: { type: :heading }, # Stop at next heading
|
|
31
|
-
# parser: :markly
|
|
32
|
-
# )
|
|
33
|
-
#
|
|
34
|
-
class PartialTemplateMerger
|
|
21
|
+
class PartialTemplateMergerBase
|
|
35
22
|
# Result of a partial template merge
|
|
36
23
|
class Result
|
|
37
24
|
# @return [String] The merged content
|
|
@@ -79,9 +66,6 @@ module Ast
|
|
|
79
66
|
# @return [Hash, nil] Boundary matcher configuration
|
|
80
67
|
attr_reader :boundary
|
|
81
68
|
|
|
82
|
-
# @return [Symbol] Parser to use (:markly, :commonmarker, etc.)
|
|
83
|
-
attr_reader :parser
|
|
84
|
-
|
|
85
69
|
# @return [Symbol, Hash] Merge preference (:template, :destination, or per-type hash)
|
|
86
70
|
attr_reader :preference
|
|
87
71
|
|
|
@@ -97,43 +81,46 @@ module Ast
|
|
|
97
81
|
# @return [Hash, nil] Node typing configuration for per-type preferences
|
|
98
82
|
attr_reader :node_typing
|
|
99
83
|
|
|
100
|
-
#
|
|
84
|
+
# @return [Object, nil] Match refiner for fuzzy matching unmatched nodes
|
|
85
|
+
attr_reader :match_refiner
|
|
86
|
+
|
|
87
|
+
# Initialize a PartialTemplateMergerBase.
|
|
101
88
|
#
|
|
102
89
|
# @param template [String] The template content (the section to merge in)
|
|
103
90
|
# @param destination [String] The destination content
|
|
104
91
|
# @param anchor [Hash] Anchor matcher: { type: :heading, text: /pattern/ }
|
|
105
92
|
# @param boundary [Hash, nil] Boundary matcher (defaults to same type as anchor)
|
|
106
|
-
# @param parser [Symbol] Parser to use (:markly, :commonmarker, :prism, :psych)
|
|
107
93
|
# @param preference [Symbol, Hash] Which content wins (:template, :destination, or per-type hash)
|
|
108
94
|
# @param add_missing [Boolean, Proc] Whether to add template nodes not in destination
|
|
109
95
|
# @param when_missing [Symbol] What to do if section not found (:skip, :append, :prepend)
|
|
110
96
|
# @param replace_mode [Boolean] If true, template replaces section entirely (no merge)
|
|
111
97
|
# @param signature_generator [Proc, nil] Custom signature generator for SmartMerger
|
|
112
98
|
# @param node_typing [Hash, nil] Node typing configuration for per-type preferences
|
|
99
|
+
# @param match_refiner [Object, nil] Match refiner for fuzzy matching (e.g., ContentMatchRefiner)
|
|
113
100
|
def initialize(
|
|
114
101
|
template:,
|
|
115
102
|
destination:,
|
|
116
103
|
anchor:,
|
|
117
104
|
boundary: nil,
|
|
118
|
-
parser: :markly,
|
|
119
105
|
preference: :template,
|
|
120
106
|
add_missing: true,
|
|
121
107
|
when_missing: :skip,
|
|
122
108
|
replace_mode: false,
|
|
123
109
|
signature_generator: nil,
|
|
124
|
-
node_typing: nil
|
|
110
|
+
node_typing: nil,
|
|
111
|
+
match_refiner: nil
|
|
125
112
|
)
|
|
126
113
|
@template = template
|
|
127
114
|
@destination = destination
|
|
128
115
|
@anchor = normalize_matcher(anchor)
|
|
129
116
|
@boundary = boundary ? normalize_matcher(boundary) : nil
|
|
130
|
-
@parser = parser
|
|
131
117
|
@preference = preference
|
|
132
118
|
@add_missing = add_missing
|
|
133
119
|
@when_missing = when_missing
|
|
134
120
|
@replace_mode = replace_mode
|
|
135
121
|
@signature_generator = signature_generator
|
|
136
122
|
@node_typing = node_typing
|
|
123
|
+
@match_refiner = match_refiner
|
|
137
124
|
end
|
|
138
125
|
|
|
139
126
|
# Perform the partial template merge.
|
|
@@ -161,6 +148,47 @@ module Ast
|
|
|
161
148
|
perform_section_merge(d_analysis, d_statements, injection_point)
|
|
162
149
|
end
|
|
163
150
|
|
|
151
|
+
protected
|
|
152
|
+
|
|
153
|
+
# Create a FileAnalysis for the given content.
|
|
154
|
+
#
|
|
155
|
+
# @abstract Subclasses must implement this method
|
|
156
|
+
# @param content [String] The content to analyze
|
|
157
|
+
# @return [Object] A FileAnalysis instance
|
|
158
|
+
def create_analysis(content)
|
|
159
|
+
raise NotImplementedError, "#{self.class} must implement #create_analysis"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Create a SmartMerger for merging the section.
|
|
163
|
+
#
|
|
164
|
+
# @abstract Subclasses must implement this method
|
|
165
|
+
# @param template_content [String] The template content
|
|
166
|
+
# @param destination_content [String] The destination section content
|
|
167
|
+
# @return [Object] A SmartMerger instance
|
|
168
|
+
def create_smart_merger(template_content, destination_content)
|
|
169
|
+
raise NotImplementedError, "#{self.class} must implement #create_smart_merger"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Find where the section ends.
|
|
173
|
+
#
|
|
174
|
+
# @abstract Subclasses must implement this method
|
|
175
|
+
# @param statements [Array<NavigableStatement>] All statements
|
|
176
|
+
# @param injection_point [InjectionPoint] The injection point
|
|
177
|
+
# @return [Integer] Index of the last statement in the section
|
|
178
|
+
def find_section_end(statements, injection_point)
|
|
179
|
+
raise NotImplementedError, "#{self.class} must implement #find_section_end"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Convert a node to its source text.
|
|
183
|
+
#
|
|
184
|
+
# @abstract Subclasses must implement this method
|
|
185
|
+
# @param node [Object] The node to convert
|
|
186
|
+
# @param analysis [Object, nil] The analysis object for source lookup
|
|
187
|
+
# @return [String] The source text
|
|
188
|
+
def node_to_text(node, analysis = nil)
|
|
189
|
+
raise NotImplementedError, "#{self.class} must implement #node_to_text"
|
|
190
|
+
end
|
|
191
|
+
|
|
164
192
|
private
|
|
165
193
|
|
|
166
194
|
def normalize_matcher(matcher)
|
|
@@ -187,17 +215,10 @@ module Ast
|
|
|
187
215
|
end
|
|
188
216
|
end
|
|
189
217
|
|
|
190
|
-
def handle_missing_section(
|
|
218
|
+
def handle_missing_section(d_analysis)
|
|
191
219
|
case when_missing
|
|
192
|
-
when :skip
|
|
193
|
-
Result.new(
|
|
194
|
-
content: destination,
|
|
195
|
-
has_section: false,
|
|
196
|
-
changed: false,
|
|
197
|
-
message: "Section not found, skipping",
|
|
198
|
-
)
|
|
199
220
|
when :append
|
|
200
|
-
# Append template
|
|
221
|
+
# Append template to end of destination
|
|
201
222
|
new_content = destination.chomp + "\n\n" + template
|
|
202
223
|
Result.new(
|
|
203
224
|
content: new_content,
|
|
@@ -206,7 +227,7 @@ module Ast
|
|
|
206
227
|
message: "Section not found, appended template",
|
|
207
228
|
)
|
|
208
229
|
when :prepend
|
|
209
|
-
# Prepend template
|
|
230
|
+
# Prepend template to beginning of destination
|
|
210
231
|
new_content = template + "\n\n" + destination
|
|
211
232
|
Result.new(
|
|
212
233
|
content: new_content,
|
|
@@ -219,12 +240,12 @@ module Ast
|
|
|
219
240
|
content: destination,
|
|
220
241
|
has_section: false,
|
|
221
242
|
changed: false,
|
|
222
|
-
message: "Section not found,
|
|
243
|
+
message: "Section not found, skipping",
|
|
223
244
|
)
|
|
224
245
|
end
|
|
225
246
|
end
|
|
226
247
|
|
|
227
|
-
def perform_section_merge(
|
|
248
|
+
def perform_section_merge(d_analysis, d_statements, injection_point)
|
|
228
249
|
# Determine section boundaries in destination
|
|
229
250
|
section_start_idx = injection_point.anchor.index
|
|
230
251
|
section_end_idx = find_section_end(d_statements, injection_point)
|
|
@@ -235,12 +256,12 @@ module Ast
|
|
|
235
256
|
after_statements = d_statements[(section_end_idx + 1)..]
|
|
236
257
|
|
|
237
258
|
# Determine the merged section content
|
|
238
|
-
section_content = statements_to_content(section_statements)
|
|
259
|
+
section_content = statements_to_content(section_statements, d_analysis)
|
|
239
260
|
merged_section, stats = merge_section_content(section_content)
|
|
240
261
|
|
|
241
|
-
# Reconstruct the document
|
|
242
|
-
before_content = statements_to_content(before_statements)
|
|
243
|
-
after_content = statements_to_content(after_statements)
|
|
262
|
+
# Reconstruct the document using source-based extraction
|
|
263
|
+
before_content = statements_to_content(before_statements, d_analysis)
|
|
264
|
+
after_content = statements_to_content(after_statements, d_analysis)
|
|
244
265
|
|
|
245
266
|
new_content = build_merged_content(before_content, merged_section, after_content)
|
|
246
267
|
|
|
@@ -280,83 +301,15 @@ module Ast
|
|
|
280
301
|
@replace_mode == true
|
|
281
302
|
end
|
|
282
303
|
|
|
283
|
-
def
|
|
284
|
-
# If boundary was specified and found, use it (exclusive - section ends before boundary)
|
|
285
|
-
if injection_point.boundary
|
|
286
|
-
return injection_point.boundary.index - 1
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
# Otherwise, find the next node of same type (for headings, same or higher level)
|
|
290
|
-
anchor = injection_point.anchor
|
|
291
|
-
anchor_type = anchor.type
|
|
292
|
-
|
|
293
|
-
# For headings, find next heading of same or higher level
|
|
294
|
-
if heading_type?(anchor_type)
|
|
295
|
-
anchor_level = get_heading_level(anchor)
|
|
296
|
-
|
|
297
|
-
((anchor.index + 1)...statements.length).each do |idx|
|
|
298
|
-
stmt = statements[idx]
|
|
299
|
-
if heading_type?(stmt.type)
|
|
300
|
-
stmt_level = get_heading_level(stmt)
|
|
301
|
-
if stmt_level && anchor_level && stmt_level <= anchor_level
|
|
302
|
-
# Found next heading of same or higher level - section ends before it
|
|
303
|
-
return idx - 1
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
else
|
|
308
|
-
# For non-headings, find next node of same type
|
|
309
|
-
((anchor.index + 1)...statements.length).each do |idx|
|
|
310
|
-
stmt = statements[idx]
|
|
311
|
-
if stmt.type == anchor_type
|
|
312
|
-
return idx - 1
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Section extends to end of document
|
|
318
|
-
statements.length - 1
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def heading_type?(type)
|
|
322
|
-
type.to_s == "heading" || type == :heading || type == :header
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
def get_heading_level(stmt)
|
|
326
|
-
inner = stmt.respond_to?(:unwrapped_node) ? stmt.unwrapped_node : stmt.node
|
|
327
|
-
|
|
328
|
-
if inner.respond_to?(:header_level)
|
|
329
|
-
inner.header_level
|
|
330
|
-
elsif inner.respond_to?(:level)
|
|
331
|
-
inner.level
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def statements_to_content(statements)
|
|
304
|
+
def statements_to_content(statements, analysis = nil)
|
|
336
305
|
return "" if statements.nil? || statements.empty?
|
|
337
306
|
|
|
338
307
|
statements.map do |stmt|
|
|
339
308
|
node = stmt.respond_to?(:node) ? stmt.node : stmt
|
|
340
|
-
node_to_text(node)
|
|
309
|
+
node_to_text(node, analysis)
|
|
341
310
|
end.join
|
|
342
311
|
end
|
|
343
312
|
|
|
344
|
-
def node_to_text(node)
|
|
345
|
-
# Unwrap if needed
|
|
346
|
-
inner = node
|
|
347
|
-
while inner.respond_to?(:inner_node) && inner.inner_node != inner
|
|
348
|
-
inner = inner.inner_node
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
if inner.respond_to?(:to_commonmark)
|
|
352
|
-
inner.to_commonmark.to_s
|
|
353
|
-
elsif inner.respond_to?(:to_s)
|
|
354
|
-
inner.to_s
|
|
355
|
-
else
|
|
356
|
-
""
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
|
|
360
313
|
def build_merged_content(before, section, after)
|
|
361
314
|
result = +""
|
|
362
315
|
|
|
@@ -388,54 +341,6 @@ module Ast
|
|
|
388
341
|
result << "\n" unless result.empty? || result.end_with?("\n")
|
|
389
342
|
result
|
|
390
343
|
end
|
|
391
|
-
|
|
392
|
-
def create_analysis(content)
|
|
393
|
-
case parser
|
|
394
|
-
when :markly
|
|
395
|
-
require "markly/merge" unless defined?(Markly::Merge)
|
|
396
|
-
Markly::Merge::FileAnalysis.new(content)
|
|
397
|
-
when :commonmarker
|
|
398
|
-
require "commonmarker/merge" unless defined?(Commonmarker::Merge)
|
|
399
|
-
Commonmarker::Merge::FileAnalysis.new(content)
|
|
400
|
-
when :prism
|
|
401
|
-
require "prism/merge" unless defined?(Prism::Merge)
|
|
402
|
-
Prism::Merge::FileAnalysis.new(content)
|
|
403
|
-
when :psych
|
|
404
|
-
require "psych/merge" unless defined?(Psych::Merge)
|
|
405
|
-
Psych::Merge::FileAnalysis.new(content)
|
|
406
|
-
else
|
|
407
|
-
raise ArgumentError, "Unknown parser: #{parser}"
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def create_smart_merger(template_content, destination_content)
|
|
412
|
-
merger_class = case parser
|
|
413
|
-
when :markly
|
|
414
|
-
require "markly/merge" unless defined?(Markly::Merge)
|
|
415
|
-
Markly::Merge::SmartMerger
|
|
416
|
-
when :commonmarker
|
|
417
|
-
require "commonmarker/merge" unless defined?(Commonmarker::Merge)
|
|
418
|
-
Commonmarker::Merge::SmartMerger
|
|
419
|
-
when :prism
|
|
420
|
-
require "prism/merge" unless defined?(Prism::Merge)
|
|
421
|
-
Prism::Merge::SmartMerger
|
|
422
|
-
when :psych
|
|
423
|
-
require "psych/merge" unless defined?(Psych::Merge)
|
|
424
|
-
Psych::Merge::SmartMerger
|
|
425
|
-
else
|
|
426
|
-
raise ArgumentError, "Unknown parser: #{parser}"
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
# Build options hash, only including non-nil values
|
|
430
|
-
options = {
|
|
431
|
-
preference: preference,
|
|
432
|
-
add_template_only_nodes: add_missing,
|
|
433
|
-
}
|
|
434
|
-
options[:signature_generator] = signature_generator if signature_generator
|
|
435
|
-
options[:node_typing] = node_typing if node_typing
|
|
436
|
-
|
|
437
|
-
merger_class.new(template_content, destination_content, **options)
|
|
438
|
-
end
|
|
439
344
|
end
|
|
440
345
|
end
|
|
441
346
|
end
|
|
@@ -124,6 +124,17 @@ module Ast
|
|
|
124
124
|
script_loader.load_callable_hash(value)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
# Get the match_refiner callable, loading from script if needed.
|
|
128
|
+
#
|
|
129
|
+
# @return [Object, nil] Match refiner instance or callable
|
|
130
|
+
def match_refiner
|
|
131
|
+
value = merge_config[:match_refiner]
|
|
132
|
+
return if value.nil?
|
|
133
|
+
return value if value.respond_to?(:call) || value.is_a?(Ast::Merge::MatchRefinerBase)
|
|
134
|
+
|
|
135
|
+
script_loader.load_callable(value)
|
|
136
|
+
end
|
|
137
|
+
|
|
127
138
|
# Convert preset to a hash suitable for SmartMerger options.
|
|
128
139
|
#
|
|
129
140
|
# @return [Hash]
|
|
@@ -133,6 +144,7 @@ module Ast
|
|
|
133
144
|
add_template_only_nodes: add_missing,
|
|
134
145
|
signature_generator: signature_generator,
|
|
135
146
|
node_typing: node_typing,
|
|
147
|
+
match_refiner: match_refiner,
|
|
136
148
|
freeze_token: freeze_token,
|
|
137
149
|
}.compact
|
|
138
150
|
end
|
|
@@ -155,6 +167,7 @@ module Ast
|
|
|
155
167
|
deep: config["deep"] == true,
|
|
156
168
|
signature_generator: config["signature_generator"],
|
|
157
169
|
node_typing: config["node_typing"],
|
|
170
|
+
match_refiner: config["match_refiner"],
|
|
158
171
|
}
|
|
159
172
|
end
|
|
160
173
|
|
|
@@ -144,19 +144,19 @@ module Ast
|
|
|
144
144
|
begin
|
|
145
145
|
destination_content = File.read(target_path)
|
|
146
146
|
|
|
147
|
-
# Use
|
|
148
|
-
merger =
|
|
147
|
+
# Use the appropriate PartialTemplateMerger based on parser
|
|
148
|
+
merger = create_partial_template_merger(
|
|
149
149
|
template: template_content,
|
|
150
150
|
destination: destination_content,
|
|
151
151
|
anchor: recipe.injection[:anchor] || {},
|
|
152
152
|
boundary: recipe.injection[:boundary],
|
|
153
|
-
parser: parser,
|
|
154
153
|
preference: recipe.preference,
|
|
155
154
|
add_missing: recipe.add_missing,
|
|
156
155
|
when_missing: recipe.when_missing,
|
|
157
156
|
replace_mode: recipe.replace_mode?,
|
|
158
157
|
signature_generator: recipe.signature_generator,
|
|
159
158
|
node_typing: recipe.node_typing,
|
|
159
|
+
match_refiner: recipe.match_refiner,
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
result = merger.merge
|
|
@@ -179,6 +179,26 @@ module Ast
|
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
181
|
|
|
182
|
+
# Create the appropriate PartialTemplateMerger based on parser type.
|
|
183
|
+
#
|
|
184
|
+
# @param options [Hash] Merger options
|
|
185
|
+
# @return [Object] A PartialTemplateMerger instance
|
|
186
|
+
def create_partial_template_merger(**options)
|
|
187
|
+
case parser.to_sym
|
|
188
|
+
when :markly, :commonmarker
|
|
189
|
+
require "markdown/merge" unless defined?(Markdown::Merge)
|
|
190
|
+
Markdown::Merge::PartialTemplateMerger.new(backend: parser, **options)
|
|
191
|
+
when :prism
|
|
192
|
+
require "prism/merge" unless defined?(Prism::Merge)
|
|
193
|
+
raise NotImplementedError, "Prism PartialTemplateMerger not yet implemented"
|
|
194
|
+
when :psych
|
|
195
|
+
require "psych/merge" unless defined?(Psych::Merge)
|
|
196
|
+
raise NotImplementedError, "Psych PartialTemplateMerger not yet implemented"
|
|
197
|
+
else
|
|
198
|
+
raise ArgumentError, "Unknown parser: #{parser}. Supported: :markly, :commonmarker"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
182
202
|
def create_result_from_merge(target_path, relative_path, _destination_content, merge_result)
|
|
183
203
|
changed = merge_result.changed
|
|
184
204
|
|
data/lib/ast/merge/version.rb
CHANGED
data/lib/ast/merge.rb
CHANGED
|
@@ -154,7 +154,7 @@ module Ast
|
|
|
154
154
|
autoload :NavigableStatement, "ast/merge/navigable_statement"
|
|
155
155
|
autoload :NodeTyping, "ast/merge/node_typing"
|
|
156
156
|
autoload :NodeWrapperBase, "ast/merge/node_wrapper_base"
|
|
157
|
-
autoload :
|
|
157
|
+
autoload :PartialTemplateMergerBase, "ast/merge/partial_template_merger_base"
|
|
158
158
|
autoload :SectionTyping, "ast/merge/section_typing"
|
|
159
159
|
autoload :SmartMergerBase, "ast/merge/smart_merger_base"
|
|
160
160
|
autoload :Text, "ast/merge/text"
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ast-merge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -66,7 +66,7 @@ dependencies:
|
|
|
66
66
|
version: '3.2'
|
|
67
67
|
- - ">="
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
|
-
version: 3.2.
|
|
69
|
+
version: 3.2.5
|
|
70
70
|
type: :runtime
|
|
71
71
|
prerelease: false
|
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -76,7 +76,7 @@ dependencies:
|
|
|
76
76
|
version: '3.2'
|
|
77
77
|
- - ">="
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
|
-
version: 3.2.
|
|
79
|
+
version: 3.2.5
|
|
80
80
|
- !ruby/object:Gem::Dependency
|
|
81
81
|
name: kettle-dev
|
|
82
82
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -322,7 +322,7 @@ files:
|
|
|
322
322
|
- lib/ast/merge/node_typing/normalizer.rb
|
|
323
323
|
- lib/ast/merge/node_typing/wrapper.rb
|
|
324
324
|
- lib/ast/merge/node_wrapper_base.rb
|
|
325
|
-
- lib/ast/merge/
|
|
325
|
+
- lib/ast/merge/partial_template_merger_base.rb
|
|
326
326
|
- lib/ast/merge/recipe.rb
|
|
327
327
|
- lib/ast/merge/recipe/config.rb
|
|
328
328
|
- lib/ast/merge/recipe/preset.rb
|
|
@@ -356,10 +356,10 @@ licenses:
|
|
|
356
356
|
- MIT
|
|
357
357
|
metadata:
|
|
358
358
|
homepage_uri: https://ast-merge.galtzo.com/
|
|
359
|
-
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/
|
|
360
|
-
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/
|
|
359
|
+
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v3.0.0
|
|
360
|
+
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v3.0.0/CHANGELOG.md
|
|
361
361
|
bug_tracker_uri: https://github.com/kettle-rb/ast-merge/issues
|
|
362
|
-
documentation_uri: https://www.rubydoc.info/gems/ast-merge/
|
|
362
|
+
documentation_uri: https://www.rubydoc.info/gems/ast-merge/3.0.0
|
|
363
363
|
funding_uri: https://github.com/sponsors/pboling
|
|
364
364
|
wiki_uri: https://github.com/kettle-rb/ast-merge/wiki
|
|
365
365
|
news_uri: https://www.railsbling.com/tags/ast-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|