ast-merge 1.0.0 → 1.1.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 +35 -0
- data/README.md +40 -27
- data/lib/ast/merge/ast_node.rb +224 -24
- data/lib/ast/merge/comment/block.rb +6 -0
- data/lib/ast/merge/comment/empty.rb +6 -0
- data/lib/ast/merge/comment/line.rb +6 -0
- data/lib/ast/merge/comment/parser.rb +9 -7
- data/lib/ast/merge/debug_logger.rb +4 -0
- data/lib/ast/merge/fenced_code_block_detector.rb +102 -0
- data/lib/ast/merge/file_analyzable.rb +5 -3
- data/lib/ast/merge/freeze_node_base.rb +1 -1
- data/lib/ast/merge/match_refiner_base.rb +1 -1
- data/lib/ast/merge/match_score_base.rb +1 -1
- data/lib/ast/merge/merger_config.rb +33 -31
- data/lib/ast/merge/section_typing.rb +52 -50
- data/lib/ast/merge/text/line_node.rb +42 -9
- data/lib/ast/merge/text/section_splitter.rb +12 -10
- data/lib/ast/merge/text/word_node.rb +47 -14
- data/lib/ast/merge/version.rb +1 -1
- data/lib/ast/merge/yaml_frontmatter_detector.rb +0 -20
- data.tar.gz.sig +0 -0
- metadata +24 -4
- 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: 86acfad0e867d5098d34fa4ec30fca07677e8b094a4bbeee27afa4dd236f6ba8
|
|
4
|
+
data.tar.gz: 61b3c5e9b24b4bc396dfa6562ebdf85d7a27153b67f96d53e977d04cc668c653
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d610aa4ba48cd7b4476c041412dcb661761464a49966af6045af8f3c0e0a6f87cb75b89d1f706caa58505fab9cdbee9aa3681f9b615a79f6db3fb31f2f25aabb
|
|
7
|
+
data.tar.gz: 5dfbdb16cebda587a4c2a460b71a3a6a794ab6dcee048d293f1b39aa679291368e221348439974782ec2cfc5cfca3b9d4f62cb12b566931e34913dfc3a8790dc
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -20,14 +20,49 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
20
20
|
|
|
21
21
|
### Added
|
|
22
22
|
|
|
23
|
+
- **tree_haver Integration**: Major architectural enhancement
|
|
24
|
+
- Added `tree_haver` (~> 3.1) as a runtime dependency
|
|
25
|
+
- `Ast::Merge::AstNode` now implements the TreeHaver::Node protocol for compatibility with tree_haver-based merge operations
|
|
26
|
+
- Adds: `type`, `kind`, `text`, `start_byte`, `end_byte`, `start_point`, `end_point`, `children`, `child_count`, `child(index)`, `each`, `named?`, `structural?`, `has_error?`, `missing?`, `inner_node`
|
|
27
|
+
- Adds `Point` struct compatible with `TreeHaver::Point`
|
|
28
|
+
- Adds `SyntheticNode` alias for clarity (synthetic = not backed by a real parser)
|
|
29
|
+
- `Comment::Line`, `Comment::Block`, `Comment::Empty` now have explicit `type` methods
|
|
30
|
+
- `Text::LineNode` and `Text::WordNode` now inherit from `AstNode`, gaining TreeHaver::Node protocol compliance
|
|
31
|
+
- This enables `*-merge` gems to leverage tree_haver's cross-Ruby parsing capabilities (MRI, JRuby, TruffleRuby)
|
|
32
|
+
- **Documentation**: Comprehensive updates across the gem family
|
|
33
|
+
- Updated all vendor gem READMEs with standardized gem family tables
|
|
34
|
+
- Added `tree_haver` as the foundation layer in architecture documentation
|
|
35
|
+
- Clarified the two-layer architecture: tree_haver (parsing) → ast-merge (merge infrastructure)
|
|
36
|
+
- Added detailed documentation to `FencedCodeBlockDetector` explaining when to use native AST nodes vs text-based detection
|
|
37
|
+
- Updated markdown-merge documentation to highlight inner code block merging capabilities
|
|
38
|
+
- **Example Scripts**: Added comprehensive examples demonstrating inner-merge capabilities
|
|
39
|
+
- `examples/markdown_code_merge.rb` - Shows how markdown-merge delegates to language-specific parsers for semantic merging
|
|
40
|
+
- Documentation proving that language-specific parsers create full ASTs of embedded code blocks
|
|
41
|
+
|
|
23
42
|
### Changed
|
|
24
43
|
|
|
44
|
+
- **Architecture**: Refactored to use tree_haver as the parsing foundation
|
|
45
|
+
- All tree-sitter-based gems (bash-merge, json-merge, jsonc-merge, toml-merge) now use tree_haver
|
|
46
|
+
- Parser-specific gems (prism-merge, psych-merge, markdown-merge, markly-merge, commonmarker-merge) use tree_haver backends
|
|
47
|
+
- Provides unified API across different Ruby implementations and parsing backends
|
|
48
|
+
- **Documentation Structure**: Standardized gem family tables across all 12 vendor gems
|
|
49
|
+
- Changed from 3-column to 4-column format: Gem | Format | Parser Backend(s) | Description
|
|
50
|
+
- All parser backends now annotated with "(via tree_haver)" where applicable
|
|
51
|
+
- ast-merge description updated from "Shared infrastructure" to "**Infrastructure**: Shared base classes and merge logic"
|
|
52
|
+
- markdown-merge description updated to "**Foundation**: Shared base for Markdown mergers with inner code block merging"
|
|
53
|
+
- **Configuration Documentation**: Enhanced backend selection documentation
|
|
54
|
+
|
|
25
55
|
### Deprecated
|
|
26
56
|
|
|
27
57
|
### Removed
|
|
28
58
|
|
|
29
59
|
### Fixed
|
|
30
60
|
|
|
61
|
+
- Fixed gemspec and Appraisals alignment with tree_haver requirements
|
|
62
|
+
- Fixed CI workflow conditions and retry logic
|
|
63
|
+
- Fixed badge rendering in documentation
|
|
64
|
+
- Fixed README structure issues (removed H3 duplicates, standardized gem family tables)
|
|
65
|
+
|
|
31
66
|
### Security
|
|
32
67
|
|
|
33
68
|
## [1.0.0] - 2025-12-12
|
data/README.md
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
# ☯️ Ast::Merge
|
|
44
44
|
|
|
45
|
-
[![Version][👽versioni]][👽version] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license-ref] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![
|
|
45
|
+
[![Version][👽versioni]][👽version] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: MIT][📄license-img]][📄license-ref] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![CodeCov Test Coverage][🏀codecovi]][🏀codecov] [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls] [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov] [![QLTY Maintainability][🏀qlty-mnti]][🏀qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![Deps Locked][🚎13-🔒️-wfi]][🚎13-🔒️-wf] [![Deps Unlocked][🚎14-🔓️-wfi]][🚎14-🔓️-wf] [![CI Supported][🚎6-s-wfi]][🚎6-s-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![CodeQL][🖐codeQL-img]][🖐codeQL] [![Apache SkyWalking Eyes License Compatibility Check][🚎15-🪪-wfi]][🚎15-🪪-wf]
|
|
46
46
|
|
|
47
47
|
`if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know][🖼️galtzo-discord], as I may have missed the [discord notification][🖼️galtzo-discord].
|
|
48
48
|
|
|
@@ -58,20 +58,23 @@ Ast::Merge is **not typically used directly** - instead, use one of the format-s
|
|
|
58
58
|
|
|
59
59
|
### The `*-merge` Gem Family
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
65
|
-
| [
|
|
66
|
-
| [
|
|
67
|
-
| [
|
|
68
|
-
| [
|
|
61
|
+
The `*-merge` gem family provides intelligent, AST-based merging for various file formats. At the foundation is [tree_haver][tree_haver], which provides a unified cross-Ruby parsing API that works seamlessly across MRI, JRuby, and TruffleRuby.
|
|
62
|
+
|
|
63
|
+
| Gem | Format | Parser Backend(s) | Description |
|
|
64
|
+
|-----|--------|-------------------|-------------|
|
|
65
|
+
| [tree_haver][tree_haver] | Multi | MRI C, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, Citrus | **Foundation**: Cross-Ruby adapter for parsing libraries (like Faraday for HTTP) |
|
|
66
|
+
| [ast-merge][ast-merge] | Text | internal | **Infrastructure**: Shared base classes and merge logic for all `*-merge` gems |
|
|
67
|
+
| [prism-merge][prism-merge] | Ruby | [Prism][prism] (via tree_haver) | Smart merge for Ruby source files |
|
|
68
|
+
| [psych-merge][psych-merge] | YAML | [Psych][psych] (via tree_haver) | Smart merge for YAML files |
|
|
69
|
+
| [json-merge][json-merge] | JSON | [tree-sitter-json][ts-json] (via tree_haver) | Smart merge for JSON files |
|
|
70
|
+
| [jsonc-merge][jsonc-merge] | JSONC | [tree-sitter-jsonc][ts-jsonc] (via tree_haver) | ⚠️ Proof of concept; Smart merge for JSON with Comments |
|
|
71
|
+
| [bash-merge][bash-merge] | Bash | [tree-sitter-bash][ts-bash] (via tree_haver) | Smart merge for Bash scripts |
|
|
69
72
|
| [rbs-merge][rbs-merge] | RBS | [RBS][rbs] | Smart merge for Ruby type signatures |
|
|
70
73
|
| [dotenv-merge][dotenv-merge] | Dotenv | internal ([dotenv][dotenv]) | Smart merge for `.env` files |
|
|
71
|
-
| [toml-merge][toml-merge] | TOML | [tree-sitter-toml][ts-toml] | Smart merge for TOML files |
|
|
72
|
-
| [markdown-merge][markdown-merge] | Markdown |
|
|
73
|
-
| [markly-merge][markly-merge] | Markdown | [Markly][markly] | Smart merge for Markdown (CommonMark via
|
|
74
|
-
| [commonmarker-merge][commonmarker-merge] | Markdown | [Commonmarker][commonmarker] | Smart merge for Markdown (CommonMark via comrak) |
|
|
74
|
+
| [toml-merge][toml-merge] | TOML | [tree-sitter-toml][ts-toml] (via tree_haver) | Smart merge for TOML files |
|
|
75
|
+
| [markdown-merge][markdown-merge] | Markdown | [Commonmarker][commonmarker] / [Markly][markly] (via tree_haver) | **Foundation**: Shared base for Markdown mergers with inner code block merging |
|
|
76
|
+
| [markly-merge][markly-merge] | Markdown | [Markly][markly] (via tree_haver) | Smart merge for Markdown (CommonMark via cmark-gfm C) |
|
|
77
|
+
| [commonmarker-merge][commonmarker-merge] | Markdown | [Commonmarker][commonmarker] (via tree_haver) | Smart merge for Markdown (CommonMark via comrak Rust) |
|
|
75
78
|
|
|
76
79
|
**Example implementations** for the gem templating use case:
|
|
77
80
|
|
|
@@ -80,6 +83,7 @@ Ast::Merge is **not typically used directly** - instead, use one of the format-s
|
|
|
80
83
|
| [kettle-dev][kettle-dev] | Gem Development | Gem templating tool using `*-merge` gems |
|
|
81
84
|
| [kettle-jem][kettle-jem] | Gem Templating | Gem template library with smart merge support |
|
|
82
85
|
|
|
86
|
+
[tree_haver]: https://github.com/kettle-rb/tree_haver
|
|
83
87
|
[ast-merge]: https://github.com/kettle-rb/ast-merge
|
|
84
88
|
[prism-merge]: https://github.com/kettle-rb/prism-merge
|
|
85
89
|
[psych-merge]: https://github.com/kettle-rb/psych-merge
|
|
@@ -102,15 +106,34 @@ Ast::Merge is **not typically used directly** - instead, use one of the format-s
|
|
|
102
106
|
[ts-toml]: https://github.com/tree-sitter-grammars/tree-sitter-toml
|
|
103
107
|
[rbs]: https://github.com/ruby/rbs
|
|
104
108
|
[dotenv]: https://github.com/bkeepers/dotenv
|
|
105
|
-
[markly]: https://github.com/
|
|
109
|
+
[markly]: https://github.com/ioquatix/markly
|
|
106
110
|
[commonmarker]: https://github.com/gjtorikian/commonmarker
|
|
107
111
|
|
|
108
|
-
###
|
|
112
|
+
### Architecture: tree_haver + ast-merge
|
|
113
|
+
|
|
114
|
+
The `*-merge` gem family is built on a two-layer architecture:
|
|
115
|
+
|
|
116
|
+
#### Layer 1: tree_haver (Parsing Foundation)
|
|
117
|
+
|
|
118
|
+
[tree_haver][tree_haver] provides cross-Ruby parsing capabilities:
|
|
119
|
+
|
|
120
|
+
- **Universal Backend Support**: Automatically selects the best parsing backend for your Ruby implementation (MRI, JRuby, TruffleRuby)
|
|
121
|
+
- **10 Backend Options**: MRI C extensions, Rust bindings, FFI, Java (JRuby), language-specific parsers (Prism, Psych, Commonmarker, Markly), and pure Ruby fallback (Citrus)
|
|
122
|
+
- **Unified API**: Write parsing code once, run on any Ruby implementation
|
|
123
|
+
- **Grammar Discovery**: Built-in `GrammarFinder` for platform-aware grammar library discovery
|
|
124
|
+
- **Thread-Safe**: Language registry with thread-safe caching
|
|
125
|
+
|
|
126
|
+
#### Layer 2: ast-merge (Merge Infrastructure)
|
|
127
|
+
|
|
128
|
+
Ast::Merge builds on tree_haver to provide:
|
|
109
129
|
|
|
110
130
|
- **Base Classes**: `FreezeNode`, `MergeResult` base classes with unified constructors
|
|
111
|
-
- **Shared Modules**: `FileAnalysisBase`, `MergerConfig`, `DebugLogger`
|
|
112
|
-
- **Freeze Block Support**: Configurable marker patterns for multiple comment syntaxes
|
|
131
|
+
- **Shared Modules**: `FileAnalysisBase`, `FileAnalyzable`, `MergerConfig`, `DebugLogger`
|
|
132
|
+
- **Freeze Block Support**: Configurable marker patterns for multiple comment syntaxes (preserve sections during merge)
|
|
133
|
+
- **Node Typing System**: `NodeTyping` for canonical node type identification across different parsers
|
|
134
|
+
- **Conflict Resolution**: `ConflictResolverBase` with pluggable strategies
|
|
113
135
|
- **Error Classes**: `ParseError`, `TemplateParseError`, `DestinationParseError`
|
|
136
|
+
- **Region Detection**: `RegionDetectorBase`, `FencedCodeBlockDetector` for text-based analysis
|
|
114
137
|
- **RSpec Shared Examples**: Test helpers for implementing new merge gems
|
|
115
138
|
|
|
116
139
|
### Creating a New Merge Gem
|
|
@@ -765,26 +788,16 @@ Thanks for RTFM. ☺️
|
|
|
765
788
|
[🏀coveralls-img]: https://coveralls.io/repos/github/kettle-rb/ast-merge/badge.svg?branch=main
|
|
766
789
|
[🖐codeQL]: https://github.com/kettle-rb/ast-merge/security/code-scanning
|
|
767
790
|
[🖐codeQL-img]: https://github.com/kettle-rb/ast-merge/actions/workflows/codeql-analysis.yml/badge.svg
|
|
768
|
-
[🚎1-an-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/ancient.yml
|
|
769
|
-
[🚎1-an-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/ancient.yml/badge.svg
|
|
770
791
|
[🚎2-cov-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/coverage.yml
|
|
771
792
|
[🚎2-cov-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/coverage.yml/badge.svg
|
|
772
793
|
[🚎3-hd-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/heads.yml
|
|
773
794
|
[🚎3-hd-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/heads.yml/badge.svg
|
|
774
|
-
[🚎4-lg-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/legacy.yml
|
|
775
|
-
[🚎4-lg-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/legacy.yml/badge.svg
|
|
776
795
|
[🚎5-st-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/style.yml
|
|
777
796
|
[🚎5-st-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/style.yml/badge.svg
|
|
778
797
|
[🚎6-s-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/supported.yml
|
|
779
798
|
[🚎6-s-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/supported.yml/badge.svg
|
|
780
|
-
[🚎7-us-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/unsupported.yml
|
|
781
|
-
[🚎7-us-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/unsupported.yml/badge.svg
|
|
782
|
-
[🚎8-ho-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/hoary.yml
|
|
783
|
-
[🚎8-ho-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/hoary.yml/badge.svg
|
|
784
799
|
[🚎9-t-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/truffle.yml
|
|
785
800
|
[🚎9-t-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/truffle.yml/badge.svg
|
|
786
|
-
[🚎10-j-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/jruby.yml
|
|
787
|
-
[🚎10-j-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/jruby.yml/badge.svg
|
|
788
801
|
[🚎11-c-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml
|
|
789
802
|
[🚎11-c-wfi]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml/badge.svg
|
|
790
803
|
[🚎12-crh-wf]: https://github.com/kettle-rb/ast-merge/actions/workflows/dep-heads.yml
|
data/lib/ast/merge/ast_node.rb
CHANGED
|
@@ -2,27 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
module Ast
|
|
4
4
|
module Merge
|
|
5
|
-
# Base class for AST nodes in the ast-merge framework.
|
|
5
|
+
# Base class for synthetic AST nodes in the ast-merge framework.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# "Synthetic" nodes are nodes that aren't backed by a real parser - they're
|
|
8
|
+
# created by ast-merge for representing content that doesn't have a native
|
|
9
|
+
# AST (comments, text lines, env file entries, etc.).
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# - #children - returns child nodes (empty array for leaf nodes)
|
|
15
|
-
# - #signature - returns a signature array for matching (optional, can use default)
|
|
11
|
+
# This class implements the TreeHaver::Node protocol, making it compatible
|
|
12
|
+
# with all code that expects TreeHaver nodes. This allows synthetic nodes
|
|
13
|
+
# to be used interchangeably with parser-backed nodes in merge operations.
|
|
16
14
|
#
|
|
17
|
-
#
|
|
15
|
+
# Implements the TreeHaver::Node protocol:
|
|
16
|
+
# - type → String node type
|
|
17
|
+
# - text / slice → Source text content
|
|
18
|
+
# - start_byte / end_byte → Byte offsets
|
|
19
|
+
# - start_point / end_point → Point (row, column)
|
|
20
|
+
# - children → Array of child nodes
|
|
21
|
+
# - named? / structural? → Node classification
|
|
22
|
+
# - inner_node → Returns self (no wrapping layer for synthetic nodes)
|
|
23
|
+
#
|
|
24
|
+
# Adds merge-specific methods:
|
|
25
|
+
# - signature → Array used for matching nodes across files
|
|
26
|
+
# - normalized_content → Cleaned text for comparison
|
|
27
|
+
#
|
|
28
|
+
# @example Subclassing for custom node types
|
|
29
|
+
# class MyNode < AstNode
|
|
30
|
+
# def type
|
|
31
|
+
# "my_node"
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# def signature
|
|
35
|
+
# [:my_node, normalized_content]
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# @see TreeHaver::Node The protocol this class implements
|
|
40
|
+
# @see Comment::Line Example synthetic node for comments
|
|
41
|
+
# @see Text::LineNode Example synthetic node for text lines
|
|
18
42
|
class AstNode
|
|
19
|
-
|
|
43
|
+
include Comparable
|
|
44
|
+
|
|
45
|
+
# Point class compatible with TreeHaver::Point
|
|
46
|
+
# Provides both method and hash-style access to row/column
|
|
47
|
+
Point = Struct.new(:row, :column, keyword_init: true) do
|
|
48
|
+
# Hash-like access for compatibility
|
|
49
|
+
def [](key)
|
|
50
|
+
case key
|
|
51
|
+
when :row, "row" then row
|
|
52
|
+
when :column, "column" then column
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{row: row, column: column}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_s
|
|
61
|
+
"(#{row}, #{column})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inspect
|
|
65
|
+
"#<Ast::Merge::AstNode::Point row=#{row} column=#{column}>"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Location struct for tracking source positions
|
|
70
|
+
# Compatible with TreeHaver location expectations
|
|
20
71
|
Location = Struct.new(:start_line, :end_line, :start_column, :end_column, keyword_init: true) do
|
|
21
72
|
# Check if a line number falls within this location
|
|
22
|
-
# @param line_number [Integer] The line number to check
|
|
73
|
+
# @param line_number [Integer] The line number to check (1-based)
|
|
23
74
|
# @return [Boolean] true if the line number is within the range
|
|
24
75
|
def cover?(line_number)
|
|
25
|
-
line_number
|
|
76
|
+
line_number.between?(start_line, end_line)
|
|
26
77
|
end
|
|
27
78
|
end
|
|
28
79
|
|
|
@@ -32,28 +83,164 @@ module Ast
|
|
|
32
83
|
# @return [String] The source text for this node
|
|
33
84
|
attr_reader :slice
|
|
34
85
|
|
|
86
|
+
# @return [String, nil] The full source text (for text extraction)
|
|
87
|
+
attr_reader :source
|
|
88
|
+
|
|
35
89
|
# Initialize a new AstNode.
|
|
36
90
|
#
|
|
37
91
|
# @param slice [String] The source text for this node
|
|
38
|
-
# @param location [Location, #start_line] Location object
|
|
39
|
-
|
|
92
|
+
# @param location [Location, #start_line] Location object
|
|
93
|
+
# @param source [String, nil] Full source text (optional)
|
|
94
|
+
def initialize(slice:, location:, source: nil)
|
|
40
95
|
@slice = slice
|
|
41
96
|
@location = location
|
|
97
|
+
@source = source
|
|
42
98
|
end
|
|
43
99
|
|
|
100
|
+
# TreeHaver::Node protocol: inner_node
|
|
101
|
+
# For synthetic nodes, this returns self (no wrapping layer)
|
|
102
|
+
#
|
|
103
|
+
# @return [AstNode] self
|
|
104
|
+
def inner_node
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# TreeHaver::Node protocol: type
|
|
109
|
+
# Returns the node type as a string.
|
|
110
|
+
# Subclasses should override this with specific type names.
|
|
111
|
+
#
|
|
112
|
+
# @return [String] Node type
|
|
113
|
+
def type
|
|
114
|
+
# Default: derive from class name (MyNode → "my_node")
|
|
115
|
+
self.class.name.split("::").last
|
|
116
|
+
.gsub(/([A-Z])/, '_\1')
|
|
117
|
+
.downcase
|
|
118
|
+
.sub(/^_/, "")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Alias for tree-sitter compatibility
|
|
122
|
+
alias_method :kind, :type
|
|
123
|
+
|
|
124
|
+
# TreeHaver::Node protocol: text
|
|
125
|
+
# @return [String] The source text
|
|
126
|
+
def text
|
|
127
|
+
slice.to_s
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# TreeHaver::Node protocol: start_byte
|
|
131
|
+
# Calculates byte offset from source if available, otherwise estimates from lines
|
|
132
|
+
#
|
|
133
|
+
# @return [Integer] Starting byte offset
|
|
134
|
+
def start_byte
|
|
135
|
+
return 0 unless source && location
|
|
136
|
+
|
|
137
|
+
# Calculate byte offset from line/column
|
|
138
|
+
lines = source.lines
|
|
139
|
+
byte_offset = 0
|
|
140
|
+
(0...(location.start_line - 1)).each do |i|
|
|
141
|
+
byte_offset += lines[i]&.bytesize || 0
|
|
142
|
+
end
|
|
143
|
+
byte_offset + (location.start_column || 0)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# TreeHaver::Node protocol: end_byte
|
|
147
|
+
#
|
|
148
|
+
# @return [Integer] Ending byte offset
|
|
149
|
+
def end_byte
|
|
150
|
+
start_byte + slice.to_s.bytesize
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# TreeHaver::Node protocol: start_point
|
|
154
|
+
# Returns a Point with row (0-based) and column
|
|
155
|
+
#
|
|
156
|
+
# @return [Point] Starting position
|
|
157
|
+
def start_point
|
|
158
|
+
Point.new(
|
|
159
|
+
row: (location&.start_line || 1) - 1, # Convert to 0-based
|
|
160
|
+
column: location&.start_column || 0,
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# TreeHaver::Node protocol: end_point
|
|
165
|
+
# Returns a Point with row (0-based) and column
|
|
166
|
+
#
|
|
167
|
+
# @return [Point] Ending position
|
|
168
|
+
def end_point
|
|
169
|
+
Point.new(
|
|
170
|
+
row: (location&.end_line || 1) - 1, # Convert to 0-based
|
|
171
|
+
column: location&.end_column || 0,
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# TreeHaver::Node protocol: children
|
|
44
176
|
# @return [Array<AstNode>] Child nodes (empty for leaf nodes)
|
|
45
177
|
def children
|
|
46
178
|
[]
|
|
47
179
|
end
|
|
48
180
|
|
|
181
|
+
# TreeHaver::Node protocol: child_count
|
|
182
|
+
# @return [Integer] Number of children
|
|
183
|
+
def child_count
|
|
184
|
+
children.size
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# TreeHaver::Node protocol: child(index)
|
|
188
|
+
# @param index [Integer] Child index
|
|
189
|
+
# @return [AstNode, nil] Child at index
|
|
190
|
+
def child(index)
|
|
191
|
+
children[index]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# TreeHaver::Node protocol: named?
|
|
195
|
+
# Synthetic nodes are always "named" (structural) nodes
|
|
196
|
+
#
|
|
197
|
+
# @return [Boolean] true
|
|
198
|
+
def named?
|
|
199
|
+
true
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# TreeHaver::Node protocol: structural?
|
|
203
|
+
# Synthetic nodes are always structural
|
|
204
|
+
#
|
|
205
|
+
# @return [Boolean] true
|
|
206
|
+
def structural?
|
|
207
|
+
true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# TreeHaver::Node protocol: has_error?
|
|
211
|
+
# Synthetic nodes don't have parse errors
|
|
212
|
+
#
|
|
213
|
+
# @return [Boolean] false
|
|
214
|
+
def has_error?
|
|
215
|
+
false
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# TreeHaver::Node protocol: missing?
|
|
219
|
+
# Synthetic nodes are never "missing"
|
|
220
|
+
#
|
|
221
|
+
# @return [Boolean] false
|
|
222
|
+
def missing?
|
|
223
|
+
false
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# TreeHaver::Node protocol: each
|
|
227
|
+
# Iterate over children
|
|
228
|
+
#
|
|
229
|
+
# @yield [AstNode] Each child node
|
|
230
|
+
# @return [Enumerator, nil]
|
|
231
|
+
def each(&block)
|
|
232
|
+
return to_enum(__method__) unless block_given?
|
|
233
|
+
children.each(&block)
|
|
234
|
+
end
|
|
235
|
+
|
|
49
236
|
# Generate a signature for this node for matching purposes.
|
|
50
237
|
#
|
|
51
238
|
# Override in subclasses for custom signature logic.
|
|
52
|
-
# Default returns the node
|
|
239
|
+
# Default returns the node type and a normalized form of the slice.
|
|
53
240
|
#
|
|
54
241
|
# @return [Array] Signature array for matching
|
|
55
242
|
def signature
|
|
56
|
-
[
|
|
243
|
+
[type.to_sym, normalized_content]
|
|
57
244
|
end
|
|
58
245
|
|
|
59
246
|
# @return [String] Normalized content for signature comparison
|
|
@@ -61,9 +248,22 @@ module Ast
|
|
|
61
248
|
slice.to_s.strip
|
|
62
249
|
end
|
|
63
250
|
|
|
251
|
+
# Comparable: compare nodes by position
|
|
252
|
+
#
|
|
253
|
+
# @param other [AstNode] node to compare with
|
|
254
|
+
# @return [Integer, nil] -1, 0, 1, or nil if not comparable
|
|
255
|
+
def <=>(other)
|
|
256
|
+
return unless other.respond_to?(:start_byte) && other.respond_to?(:end_byte)
|
|
257
|
+
|
|
258
|
+
cmp = start_byte <=> other.start_byte
|
|
259
|
+
return cmp if cmp.nonzero?
|
|
260
|
+
|
|
261
|
+
end_byte <=> other.end_byte
|
|
262
|
+
end
|
|
263
|
+
|
|
64
264
|
# @return [String] Human-readable representation
|
|
65
265
|
def inspect
|
|
66
|
-
"#<#{self.class.name} lines=#{location
|
|
266
|
+
"#<#{self.class.name} type=#{type} lines=#{location&.start_line}..#{location&.end_line}>"
|
|
67
267
|
end
|
|
68
268
|
|
|
69
269
|
# @return [String] The source text
|
|
@@ -76,12 +276,12 @@ module Ast
|
|
|
76
276
|
def unwrap
|
|
77
277
|
self
|
|
78
278
|
end
|
|
79
|
-
|
|
80
|
-
# Check if this node responds to the Prism-style location API
|
|
81
|
-
# @return [Boolean] true
|
|
82
|
-
def respond_to_missing?(method, include_private = false)
|
|
83
|
-
[:location, :slice].include?(method) || super
|
|
84
|
-
end
|
|
85
279
|
end
|
|
280
|
+
|
|
281
|
+
# Alias for clarity - SyntheticNode clearly indicates "not backed by a real parser"
|
|
282
|
+
# Use this alias when the distinction between synthetic and parser-backed nodes matters.
|
|
283
|
+
#
|
|
284
|
+
# @see AstNode
|
|
285
|
+
SyntheticNode = AstNode
|
|
86
286
|
end
|
|
87
287
|
end
|
|
@@ -41,6 +41,12 @@ module Ast
|
|
|
41
41
|
# @return [Style] The comment style configuration
|
|
42
42
|
attr_reader :style
|
|
43
43
|
|
|
44
|
+
# TreeHaver::Node protocol: type
|
|
45
|
+
# @return [String] "comment_block"
|
|
46
|
+
def type
|
|
47
|
+
"comment_block"
|
|
48
|
+
end
|
|
49
|
+
|
|
44
50
|
# Initialize a new Block.
|
|
45
51
|
#
|
|
46
52
|
# For line-based comments, pass `children` array.
|
|
@@ -28,6 +28,12 @@ module Ast
|
|
|
28
28
|
# @return [String] The actual line content (may have whitespace)
|
|
29
29
|
attr_reader :text
|
|
30
30
|
|
|
31
|
+
# TreeHaver::Node protocol: type
|
|
32
|
+
# @return [String] "empty_line"
|
|
33
|
+
def type
|
|
34
|
+
"empty_line"
|
|
35
|
+
end
|
|
36
|
+
|
|
31
37
|
# Initialize a new Empty line.
|
|
32
38
|
#
|
|
33
39
|
# @param line_number [Integer] The 1-based line number
|
|
@@ -38,6 +38,12 @@ module Ast
|
|
|
38
38
|
# @return [Style] The comment style configuration
|
|
39
39
|
attr_reader :style
|
|
40
40
|
|
|
41
|
+
# TreeHaver::Node protocol: type
|
|
42
|
+
# @return [String] "comment_line"
|
|
43
|
+
def type
|
|
44
|
+
"comment_line"
|
|
45
|
+
end
|
|
46
|
+
|
|
41
47
|
# Initialize a new Line.
|
|
42
48
|
#
|
|
43
49
|
# @param text [String] The full comment text including delimiter
|
|
@@ -69,13 +69,15 @@ module Ast
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
class << self
|
|
73
|
+
# Parse lines as comments.
|
|
74
|
+
#
|
|
75
|
+
# @param lines [Array<String>] Source lines
|
|
76
|
+
# @param style [Style, Symbol, nil] Comment style
|
|
77
|
+
# @return [Array<AstNode>] Parsed nodes
|
|
78
|
+
def parse(lines, style: nil)
|
|
79
|
+
new(lines, style: style).parse
|
|
80
|
+
end
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
private
|
|
@@ -83,10 +83,14 @@ module Ast
|
|
|
83
83
|
|
|
84
84
|
class << self
|
|
85
85
|
# @return [String] Environment variable name to check for debug mode
|
|
86
|
+
# rubocop:disable ThreadSafety/ClassAndModuleAttributes - Configuration attribute, set once at load time
|
|
86
87
|
attr_accessor :env_var_name
|
|
88
|
+
# rubocop:enable ThreadSafety/ClassAndModuleAttributes
|
|
87
89
|
|
|
88
90
|
# @return [String] Prefix for log messages
|
|
91
|
+
# rubocop:disable ThreadSafety/ClassAndModuleAttributes - Configuration attribute, set once at load time
|
|
89
92
|
attr_accessor :log_prefix
|
|
93
|
+
# rubocop:enable ThreadSafety/ClassAndModuleAttributes
|
|
90
94
|
|
|
91
95
|
# Hook called when a module extends Ast::Merge::DebugLogger.
|
|
92
96
|
# Sets up attr_accessor for env_var_name and log_prefix on the extending module,
|
|
@@ -8,6 +8,108 @@ module Ast
|
|
|
8
8
|
# that have a specific language identifier. It can be configured for any
|
|
9
9
|
# language: ruby, json, yaml, mermaid, etc.
|
|
10
10
|
#
|
|
11
|
+
# ## When to Use This Detector
|
|
12
|
+
#
|
|
13
|
+
# **Use FencedCodeBlockDetector when:**
|
|
14
|
+
# - Working with raw Markdown text without parsing to AST
|
|
15
|
+
# - Quick extraction from strings without parser dependencies
|
|
16
|
+
# - Custom text processing requiring line-level precision
|
|
17
|
+
# - Operating on source text directly (e.g., linters, formatters)
|
|
18
|
+
#
|
|
19
|
+
# **Do NOT use FencedCodeBlockDetector when:**
|
|
20
|
+
# - Working with parsed Markdown AST (use native code block nodes instead)
|
|
21
|
+
# - Integrating with markdown-merge's CodeBlockMerger (it uses native nodes)
|
|
22
|
+
# - Using tree_haver's unified Markdown backend API
|
|
23
|
+
#
|
|
24
|
+
# ## Comparison: FencedCodeBlockDetector vs Native AST Nodes
|
|
25
|
+
#
|
|
26
|
+
# ### Native AST Approach (Preferred for AST-based Tools)
|
|
27
|
+
#
|
|
28
|
+
# When working with parsed Markdown AST via tree_haver (commonmarker/markly backends):
|
|
29
|
+
#
|
|
30
|
+
# ```ruby
|
|
31
|
+
# # markdown-merge's CodeBlockMerger uses this approach:
|
|
32
|
+
# language = node.fence_info.split(/\s+/).first # e.g., "ruby"
|
|
33
|
+
# content = node.string_content # Raw code inside block
|
|
34
|
+
#
|
|
35
|
+
# # Then delegate to language-specific parser:
|
|
36
|
+
# case language
|
|
37
|
+
# when "ruby"
|
|
38
|
+
# merger = Prism::Merge::SmartMerger.new(template, dest, preference: :destination)
|
|
39
|
+
# merged_content = merger.merge # Prism parses Ruby code into full AST!
|
|
40
|
+
# when "yaml"
|
|
41
|
+
# merger = Psych::Merge::SmartMerger.new(template, dest, preference: :destination)
|
|
42
|
+
# merged_content = merger.merge # Psych parses YAML into AST!
|
|
43
|
+
# when "json"
|
|
44
|
+
# merger = Json::Merge::SmartMerger.new(template, dest, preference: :destination)
|
|
45
|
+
# merged_content = merger.merge # JSON parser creates AST!
|
|
46
|
+
# when "bash"
|
|
47
|
+
# merger = Bash::Merge::SmartMerger.new(template, dest, preference: :destination)
|
|
48
|
+
# merged_content = merger.merge # tree-sitter parses bash into AST!
|
|
49
|
+
# end
|
|
50
|
+
# ```
|
|
51
|
+
#
|
|
52
|
+
# **Advantages of Native AST approach:**
|
|
53
|
+
# - ✓ Parser handles all edge cases (nested backticks, indentation, etc.)
|
|
54
|
+
# - ✓ Respects node boundaries from authoritative source
|
|
55
|
+
# - ✓ No regex brittleness
|
|
56
|
+
# - ✓ Automatic handling of ``` and ~~~ fence styles
|
|
57
|
+
# - ✓ Enables TRUE language-aware merging (not just text replacement)
|
|
58
|
+
# - ✓ Language-specific parsers create full ASTs of embedded code
|
|
59
|
+
# - ✓ Smart merging at semantic level (method definitions, YAML keys, JSON properties)
|
|
60
|
+
#
|
|
61
|
+
# ### Text-Based Approach (This Class)
|
|
62
|
+
#
|
|
63
|
+
# When working with raw text:
|
|
64
|
+
#
|
|
65
|
+
# ```ruby
|
|
66
|
+
# detector = FencedCodeBlockDetector.ruby
|
|
67
|
+
# regions = detector.detect_all(markdown_text)
|
|
68
|
+
# regions.each do |region|
|
|
69
|
+
# puts "Ruby code at lines #{region.start_line}-#{region.end_line}"
|
|
70
|
+
# # region.content is just a string - NO parsing happens
|
|
71
|
+
# end
|
|
72
|
+
# ```
|
|
73
|
+
#
|
|
74
|
+
# **Limitations of text-based approach:**
|
|
75
|
+
# - • Uses regex to find blocks (may miss edge cases)
|
|
76
|
+
# - • Returns strings, not parsed structures
|
|
77
|
+
# - • Cannot perform semantic merging
|
|
78
|
+
# - • Manual handling of fence variations
|
|
79
|
+
# - • No language-specific intelligence
|
|
80
|
+
#
|
|
81
|
+
# ## Real-World Example: markdown-merge Inner Code Block Merging
|
|
82
|
+
#
|
|
83
|
+
# When `inner_merge_code_blocks: true` is enabled in markdown-merge:
|
|
84
|
+
#
|
|
85
|
+
# 1. **Markdown Parser** (commonmarker/markly) parses markdown into AST
|
|
86
|
+
# - Creates code_block nodes with `fence_info` and `string_content`
|
|
87
|
+
#
|
|
88
|
+
# 2. **CodeBlockMerger** extracts code using native node properties:
|
|
89
|
+
# ```ruby
|
|
90
|
+
# language = node.fence_info.split(/\s+/).first
|
|
91
|
+
# template_code = template_node.string_content
|
|
92
|
+
# dest_code = dest_node.string_content
|
|
93
|
+
# ```
|
|
94
|
+
#
|
|
95
|
+
# 3. **Language-Specific Parser** creates FULL AST of the embedded code:
|
|
96
|
+
# - `Prism::Merge` → Prism parses Ruby into complete AST (ClassNode, DefNode, etc.)
|
|
97
|
+
# - `Psych::Merge` → Psych parses YAML into document structure
|
|
98
|
+
# - `Json::Merge` → JSON parser creates object/array tree
|
|
99
|
+
# - `Bash::Merge` → tree-sitter creates bash statement AST
|
|
100
|
+
#
|
|
101
|
+
# 4. **Smart Merger** performs SEMANTIC merging at AST level:
|
|
102
|
+
# - Ruby: Merges class definitions, preserves custom methods
|
|
103
|
+
# - YAML: Merges keys, preserves custom configuration values
|
|
104
|
+
# - JSON: Merges objects, destination values win on conflicts
|
|
105
|
+
# - Bash: Merges statements, preserves custom exports
|
|
106
|
+
#
|
|
107
|
+
# 5. **Result** is intelligently merged code, not simple text concatenation!
|
|
108
|
+
#
|
|
109
|
+
# **This means:** The embedded code is FULLY PARSED by its native language parser,
|
|
110
|
+
# enabling true semantic-level merging. FencedCodeBlockDetector would only find
|
|
111
|
+
# the text boundaries - it cannot perform this semantic merging.
|
|
112
|
+
#
|
|
11
113
|
# @example Detecting Ruby code blocks
|
|
12
114
|
# detector = FencedCodeBlockDetector.new("ruby", aliases: ["rb"])
|
|
13
115
|
# regions = detector.detect_all(markdown_source)
|
|
@@ -48,9 +48,11 @@ module Ast
|
|
|
48
48
|
# @return [String] Token used to mark freeze blocks (e.g., "prism-merge", "psych-merge")
|
|
49
49
|
# @!attribute [r] signature_generator
|
|
50
50
|
# @return [Proc, nil] Custom signature generator, or nil to use default
|
|
51
|
-
|
|
52
|
-
base
|
|
53
|
-
|
|
51
|
+
class << self
|
|
52
|
+
def included(base)
|
|
53
|
+
base.class_eval do
|
|
54
|
+
attr_reader(:source, :lines, :freeze_token, :signature_generator)
|
|
55
|
+
end
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
|
|
@@ -379,7 +379,7 @@ module Ast
|
|
|
379
379
|
# Validate that end_line is not before start_line
|
|
380
380
|
# @raise [InvalidStructureError] if structure is invalid
|
|
381
381
|
def validate_line_order!
|
|
382
|
-
return
|
|
382
|
+
return if @end_line >= @start_line
|
|
383
383
|
|
|
384
384
|
raise InvalidStructureError.new(
|
|
385
385
|
"Freeze block end line (#{@end_line}) is before start line (#{@start_line})",
|
|
@@ -169,7 +169,7 @@ module Ast
|
|
|
169
169
|
# @param threshold [Float] Minimum score to accept a match (0.0-1.0)
|
|
170
170
|
# @param node_types [Array<Symbol>] Node types to process (empty = all)
|
|
171
171
|
def initialize(threshold: DEFAULT_THRESHOLD, node_types: [])
|
|
172
|
-
@threshold =
|
|
172
|
+
@threshold = threshold.to_f.clamp(0.0, 1.0)
|
|
173
173
|
@node_types = Array(node_types)
|
|
174
174
|
end
|
|
175
175
|
|
|
@@ -193,38 +193,40 @@ module Ast
|
|
|
193
193
|
)
|
|
194
194
|
end
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
196
|
+
class << self
|
|
197
|
+
# Create a config preset for "destination wins" merging.
|
|
198
|
+
# Destination customizations are preserved, template-only content is skipped.
|
|
199
|
+
#
|
|
200
|
+
# @param freeze_token [String, nil] Optional freeze token
|
|
201
|
+
# @param signature_generator [Proc, nil] Optional signature generator
|
|
202
|
+
# @param node_typing [Hash, nil] Optional node typing configuration
|
|
203
|
+
# @return [MergerConfig] Config preset
|
|
204
|
+
def destination_wins(freeze_token: nil, signature_generator: nil, node_typing: nil)
|
|
205
|
+
new(
|
|
206
|
+
preference: :destination,
|
|
207
|
+
add_template_only_nodes: false,
|
|
208
|
+
freeze_token: freeze_token,
|
|
209
|
+
signature_generator: signature_generator,
|
|
210
|
+
node_typing: node_typing,
|
|
211
|
+
)
|
|
212
|
+
end
|
|
212
213
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
214
|
+
# Create a config preset for "template wins" merging.
|
|
215
|
+
# Template updates are applied, template-only content is added.
|
|
216
|
+
#
|
|
217
|
+
# @param freeze_token [String, nil] Optional freeze token
|
|
218
|
+
# @param signature_generator [Proc, nil] Optional signature generator
|
|
219
|
+
# @param node_typing [Hash, nil] Optional node typing configuration
|
|
220
|
+
# @return [MergerConfig] Config preset
|
|
221
|
+
def template_wins(freeze_token: nil, signature_generator: nil, node_typing: nil)
|
|
222
|
+
new(
|
|
223
|
+
preference: :template,
|
|
224
|
+
add_template_only_nodes: true,
|
|
225
|
+
freeze_token: freeze_token,
|
|
226
|
+
signature_generator: signature_generator,
|
|
227
|
+
node_typing: node_typing,
|
|
228
|
+
)
|
|
229
|
+
end
|
|
228
230
|
end
|
|
229
231
|
|
|
230
232
|
private
|
|
@@ -235,68 +235,70 @@ module Ast
|
|
|
235
235
|
# Merge typed sections from template and destination.
|
|
236
236
|
#
|
|
237
237
|
# Similar to `Text::SectionSplitter#merge_section_lists` but works with
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
238
|
+
class << self
|
|
239
|
+
# TypedSection objects wrapping AST nodes.
|
|
240
|
+
#
|
|
241
|
+
# @param template_sections [Array<TypedSection>] Sections from template
|
|
242
|
+
# @param dest_sections [Array<TypedSection>] Sections from destination
|
|
243
|
+
# @param preference [Symbol, Hash] Merge preference (:template, :destination, or per-section Hash)
|
|
244
|
+
# @param add_template_only [Boolean] Whether to add sections only in template
|
|
245
|
+
# @return [Array<TypedSection>] Merged sections
|
|
246
|
+
def merge_sections(template_sections, dest_sections, preference: :destination, add_template_only: false)
|
|
247
|
+
dest_by_name = dest_sections.each_with_object({}) do |section, hash|
|
|
248
|
+
key = section.normalized_name
|
|
249
|
+
hash[key] = section unless section.unclassified?
|
|
250
|
+
end
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
merged = []
|
|
253
|
+
seen_names = Set.new
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
255
|
+
template_sections.each do |template_section|
|
|
256
|
+
if template_section.unclassified?
|
|
257
|
+
# Unclassified sections are typically kept as-is or merged specially
|
|
258
|
+
merged << template_section if add_template_only
|
|
259
|
+
next
|
|
260
|
+
end
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
key = template_section.normalized_name
|
|
263
|
+
seen_names << key
|
|
263
264
|
|
|
264
|
-
|
|
265
|
+
dest_section = dest_by_name[key]
|
|
265
266
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
if dest_section
|
|
268
|
+
# Section exists in both - choose based on preference
|
|
269
|
+
section_pref = preference_for(template_section.name, preference)
|
|
270
|
+
merged << ((section_pref == :template) ? template_section : dest_section)
|
|
271
|
+
elsif add_template_only
|
|
272
|
+
merged << template_section
|
|
273
|
+
end
|
|
272
274
|
end
|
|
273
|
-
end
|
|
274
275
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
# Append destination-only sections
|
|
277
|
+
dest_sections.each do |dest_section|
|
|
278
|
+
next if dest_section.unclassified?
|
|
279
|
+
key = dest_section.normalized_name
|
|
280
|
+
next if seen_names.include?(key)
|
|
281
|
+
merged << dest_section
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
merged
|
|
281
285
|
end
|
|
282
286
|
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
# Get preference for a specific section.
|
|
288
|
+
#
|
|
289
|
+
# @param section_name [String, Symbol] The section name
|
|
290
|
+
# @param preference [Symbol, Hash] Overall preference
|
|
291
|
+
# @return [Symbol] :template or :destination
|
|
292
|
+
def preference_for(section_name, preference)
|
|
293
|
+
return preference unless preference.is_a?(Hash)
|
|
285
294
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
# @return [Symbol] :template or :destination
|
|
291
|
-
def self.preference_for(section_name, preference)
|
|
292
|
-
return preference unless preference.is_a?(Hash)
|
|
295
|
+
normalized = section_name.to_s.strip.downcase
|
|
296
|
+
preference.each do |key, value|
|
|
297
|
+
return value if key.to_s.strip.downcase == normalized
|
|
298
|
+
end
|
|
293
299
|
|
|
294
|
-
|
|
295
|
-
preference.each do |key, value|
|
|
296
|
-
return value if key.to_s.strip.downcase == normalized
|
|
300
|
+
preference.fetch(:default, :destination)
|
|
297
301
|
end
|
|
298
|
-
|
|
299
|
-
preference.fetch(:default, :destination)
|
|
300
302
|
end
|
|
301
303
|
end
|
|
302
304
|
end
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../ast_node"
|
|
4
|
+
|
|
3
5
|
module Ast
|
|
4
6
|
module Merge
|
|
5
7
|
module Text
|
|
6
8
|
# Represents a line of text in the text-based AST.
|
|
7
9
|
# Lines are top-level nodes, with words as nested children.
|
|
8
10
|
#
|
|
11
|
+
# Inherits from AstNode (SyntheticNode) to implement the TreeHaver::Node
|
|
12
|
+
# protocol, making it compatible with all tree_haver-based merge operations.
|
|
13
|
+
#
|
|
9
14
|
# @example
|
|
10
15
|
# line = LineNode.new("Hello world!", line_number: 1)
|
|
11
16
|
# line.content # => "Hello world!"
|
|
12
17
|
# line.words.size # => 2
|
|
13
18
|
# line.signature # => [:line, "Hello world!"]
|
|
14
|
-
|
|
19
|
+
# line.type # => "line_node" (TreeHaver protocol)
|
|
20
|
+
# line.text # => "Hello world!" (TreeHaver protocol)
|
|
21
|
+
class LineNode < AstNode
|
|
15
22
|
# @return [String] The full line content (without trailing newline)
|
|
16
23
|
attr_reader :content
|
|
17
24
|
|
|
18
|
-
# @return [Integer] 1-based line number
|
|
19
|
-
attr_reader :line_number
|
|
20
|
-
|
|
21
25
|
# @return [Array<WordNode>] Words contained in this line
|
|
22
26
|
attr_reader :words
|
|
23
27
|
|
|
@@ -27,10 +31,33 @@ module Ast
|
|
|
27
31
|
# @param line_number [Integer] 1-based line number
|
|
28
32
|
def initialize(content, line_number:)
|
|
29
33
|
@content = content
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
location = AstNode::Location.new(
|
|
36
|
+
start_line: line_number,
|
|
37
|
+
end_line: line_number,
|
|
38
|
+
start_column: 0,
|
|
39
|
+
end_column: content.length,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
super(slice: content, location: location)
|
|
43
|
+
|
|
44
|
+
# Parse words AFTER super sets up location
|
|
31
45
|
@words = parse_words
|
|
32
46
|
end
|
|
33
47
|
|
|
48
|
+
# TreeHaver::Node protocol: type
|
|
49
|
+
# @return [String] "line_node"
|
|
50
|
+
def type
|
|
51
|
+
"line_node"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# TreeHaver::Node protocol: children
|
|
55
|
+
# Returns word nodes as children
|
|
56
|
+
# @return [Array<WordNode>]
|
|
57
|
+
def children
|
|
58
|
+
@words
|
|
59
|
+
end
|
|
60
|
+
|
|
34
61
|
# Generate a signature for this line node.
|
|
35
62
|
# The signature is used for matching lines across template/destination.
|
|
36
63
|
#
|
|
@@ -61,18 +88,24 @@ module Ast
|
|
|
61
88
|
@content.strip.start_with?("#")
|
|
62
89
|
end
|
|
63
90
|
|
|
91
|
+
# Get the 1-based line number
|
|
92
|
+
# @return [Integer] 1-based line number
|
|
93
|
+
def line_number
|
|
94
|
+
location.start_line
|
|
95
|
+
end
|
|
96
|
+
|
|
64
97
|
# Get the starting line (for compatibility with AST node interface)
|
|
65
98
|
#
|
|
66
99
|
# @return [Integer] 1-based start line
|
|
67
100
|
def start_line
|
|
68
|
-
|
|
101
|
+
location.start_line
|
|
69
102
|
end
|
|
70
103
|
|
|
71
104
|
# Get the ending line (for compatibility with AST node interface)
|
|
72
105
|
#
|
|
73
106
|
# @return [Integer] 1-based end line (same as start for single line)
|
|
74
107
|
def end_line
|
|
75
|
-
|
|
108
|
+
location.end_line
|
|
76
109
|
end
|
|
77
110
|
|
|
78
111
|
# Check equality with another LineNode
|
|
@@ -96,7 +129,7 @@ module Ast
|
|
|
96
129
|
#
|
|
97
130
|
# @return [String] Debug representation
|
|
98
131
|
def inspect
|
|
99
|
-
"#<LineNode line=#{
|
|
132
|
+
"#<LineNode line=#{line_number} #{@content.inspect} words=#{@words.size}>"
|
|
100
133
|
end
|
|
101
134
|
|
|
102
135
|
# Convert to string (returns content)
|
|
@@ -126,7 +159,7 @@ module Ast
|
|
|
126
159
|
|
|
127
160
|
words << WordNode.new(
|
|
128
161
|
word,
|
|
129
|
-
line_number:
|
|
162
|
+
line_number: line_number,
|
|
130
163
|
word_index: word_index,
|
|
131
164
|
start_col: start_col,
|
|
132
165
|
end_col: end_col,
|
|
@@ -265,16 +265,18 @@ module Ast
|
|
|
265
265
|
normalize_name(section.name)
|
|
266
266
|
end
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
268
|
+
class << self
|
|
269
|
+
# Validate a splitter configuration.
|
|
270
|
+
#
|
|
271
|
+
# @param config [Hash, nil] Configuration to validate
|
|
272
|
+
# @raise [ArgumentError] If configuration is invalid
|
|
273
|
+
# @return [void]
|
|
274
|
+
def validate!(config)
|
|
275
|
+
return if config.nil?
|
|
276
|
+
|
|
277
|
+
unless config.is_a?(Hash)
|
|
278
|
+
raise ArgumentError, "splitter config must be a Hash, got #{config.class}"
|
|
279
|
+
end
|
|
278
280
|
end
|
|
279
281
|
end
|
|
280
282
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../ast_node"
|
|
4
|
+
|
|
3
5
|
module Ast
|
|
4
6
|
module Merge
|
|
5
7
|
module Text
|
|
@@ -7,26 +9,21 @@ module Ast
|
|
|
7
9
|
# Words are the nested level of the text-based AST.
|
|
8
10
|
# They are identified by word boundaries (regex \b).
|
|
9
11
|
#
|
|
12
|
+
# Inherits from AstNode (SyntheticNode) to implement the TreeHaver::Node
|
|
13
|
+
# protocol, making it compatible with all tree_haver-based merge operations.
|
|
14
|
+
#
|
|
10
15
|
# @example
|
|
11
16
|
# word = WordNode.new("hello", line_number: 1, word_index: 0, start_col: 0, end_col: 5)
|
|
12
17
|
# word.content # => "hello"
|
|
13
18
|
# word.signature # => [:word, "hello"]
|
|
14
|
-
|
|
19
|
+
# word.type # => "word_node" (TreeHaver protocol)
|
|
20
|
+
class WordNode < AstNode
|
|
15
21
|
# @return [String] The word content
|
|
16
22
|
attr_reader :content
|
|
17
23
|
|
|
18
|
-
# @return [Integer] 1-based line number containing this word
|
|
19
|
-
attr_reader :line_number
|
|
20
|
-
|
|
21
24
|
# @return [Integer] 0-based index of this word within the line
|
|
22
25
|
attr_reader :word_index
|
|
23
26
|
|
|
24
|
-
# @return [Integer] 0-based starting column position
|
|
25
|
-
attr_reader :start_col
|
|
26
|
-
|
|
27
|
-
# @return [Integer] 0-based ending column position (exclusive)
|
|
28
|
-
attr_reader :end_col
|
|
29
|
-
|
|
30
27
|
# Initialize a new WordNode
|
|
31
28
|
#
|
|
32
29
|
# @param content [String] The word content
|
|
@@ -36,10 +33,22 @@ module Ast
|
|
|
36
33
|
# @param end_col [Integer] 0-based end column (exclusive)
|
|
37
34
|
def initialize(content, line_number:, word_index:, start_col:, end_col:)
|
|
38
35
|
@content = content
|
|
39
|
-
@line_number = line_number
|
|
40
36
|
@word_index = word_index
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
|
|
38
|
+
location = AstNode::Location.new(
|
|
39
|
+
start_line: line_number,
|
|
40
|
+
end_line: line_number,
|
|
41
|
+
start_column: start_col,
|
|
42
|
+
end_column: end_col,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
super(slice: content, location: location)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# TreeHaver::Node protocol: type
|
|
49
|
+
# @return [String] "word_node"
|
|
50
|
+
def type
|
|
51
|
+
"word_node"
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
# Generate a signature for this word node.
|
|
@@ -50,6 +59,30 @@ module Ast
|
|
|
50
59
|
[:word, @content]
|
|
51
60
|
end
|
|
52
61
|
|
|
62
|
+
# Get normalized content (the word itself for words)
|
|
63
|
+
# @return [String]
|
|
64
|
+
def normalized_content
|
|
65
|
+
@content
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get the 1-based line number
|
|
69
|
+
# @return [Integer]
|
|
70
|
+
def line_number
|
|
71
|
+
location.start_line
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get start column (0-based)
|
|
75
|
+
# @return [Integer]
|
|
76
|
+
def start_col
|
|
77
|
+
location.start_column
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get end column (0-based, exclusive)
|
|
81
|
+
# @return [Integer]
|
|
82
|
+
def end_col
|
|
83
|
+
location.end_column
|
|
84
|
+
end
|
|
85
|
+
|
|
53
86
|
# Check equality with another WordNode
|
|
54
87
|
#
|
|
55
88
|
# @param other [WordNode] Other node to compare
|
|
@@ -71,7 +104,7 @@ module Ast
|
|
|
71
104
|
#
|
|
72
105
|
# @return [String] Debug representation
|
|
73
106
|
def inspect
|
|
74
|
-
"#<WordNode #{@content.inspect} line=#{
|
|
107
|
+
"#<WordNode #{@content.inspect} line=#{line_number} col=#{start_col}..#{end_col}>"
|
|
75
108
|
end
|
|
76
109
|
|
|
77
110
|
# Convert to string (returns content)
|
data/lib/ast/merge/version.rb
CHANGED
|
@@ -56,26 +56,6 @@ module Ast
|
|
|
56
56
|
# Calculate line numbers
|
|
57
57
|
# Frontmatter starts at line 1 (or after BOM)
|
|
58
58
|
start_line = 1
|
|
59
|
-
# Count newlines in content to determine end line
|
|
60
|
-
# Opening delimiter ends at line 1
|
|
61
|
-
# Content spans from line 2 to line 2 + content_lines - 1
|
|
62
|
-
# Closing delimiter is on the next line
|
|
63
|
-
content_newlines = content.count("\n")
|
|
64
|
-
# end_line is the line with the closing ---
|
|
65
|
-
end_line = start_line + 1 + content_newlines
|
|
66
|
-
|
|
67
|
-
# Adjust if content ends without newline
|
|
68
|
-
end_line - 1 if content.end_with?("\n") && content_newlines > 0
|
|
69
|
-
|
|
70
|
-
# Actually, let's calculate more carefully
|
|
71
|
-
# Line 1: ---
|
|
72
|
-
# Line 2 to N: content
|
|
73
|
-
# Line N+1: ---
|
|
74
|
-
if content.empty?
|
|
75
|
-
0
|
|
76
|
-
else
|
|
77
|
-
content.count("\n") + (content.end_with?("\n") ? 0 : 1)
|
|
78
|
-
end
|
|
79
59
|
|
|
80
60
|
# Simplify: count total newlines in the full match to determine end line
|
|
81
61
|
full_match = match[0]
|
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: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -57,6 +57,26 @@ dependencies:
|
|
|
57
57
|
- - ">="
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
59
|
version: 1.1.9
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: tree_haver
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.1'
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 3.1.0
|
|
70
|
+
type: :runtime
|
|
71
|
+
prerelease: false
|
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - "~>"
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '3.1'
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 3.1.0
|
|
60
80
|
- !ruby/object:Gem::Dependency
|
|
61
81
|
name: kettle-dev
|
|
62
82
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -288,10 +308,10 @@ licenses:
|
|
|
288
308
|
- MIT
|
|
289
309
|
metadata:
|
|
290
310
|
homepage_uri: https://ast-merge.galtzo.com/
|
|
291
|
-
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v1.
|
|
292
|
-
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v1.
|
|
311
|
+
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v1.1.0
|
|
312
|
+
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v1.1.0/CHANGELOG.md
|
|
293
313
|
bug_tracker_uri: https://github.com/kettle-rb/ast-merge/issues
|
|
294
|
-
documentation_uri: https://www.rubydoc.info/gems/ast-merge/1.
|
|
314
|
+
documentation_uri: https://www.rubydoc.info/gems/ast-merge/1.1.0
|
|
295
315
|
funding_uri: https://github.com/sponsors/pboling
|
|
296
316
|
wiki_uri: https://github.com/kettle-rb/ast-merge/wiki
|
|
297
317
|
news_uri: https://www.railsbling.com/tags/ast-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|