markdown-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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +251 -0
  4. data/CITATION.cff +20 -0
  5. data/CODE_OF_CONDUCT.md +134 -0
  6. data/CONTRIBUTING.md +227 -0
  7. data/FUNDING.md +74 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +1087 -0
  10. data/REEK +0 -0
  11. data/RUBOCOP.md +71 -0
  12. data/SECURITY.md +21 -0
  13. data/lib/markdown/merge/cleanse/block_spacing.rb +253 -0
  14. data/lib/markdown/merge/cleanse/code_fence_spacing.rb +294 -0
  15. data/lib/markdown/merge/cleanse/condensed_link_refs.rb +405 -0
  16. data/lib/markdown/merge/cleanse.rb +42 -0
  17. data/lib/markdown/merge/code_block_merger.rb +300 -0
  18. data/lib/markdown/merge/conflict_resolver.rb +128 -0
  19. data/lib/markdown/merge/debug_logger.rb +26 -0
  20. data/lib/markdown/merge/document_problems.rb +190 -0
  21. data/lib/markdown/merge/file_aligner.rb +196 -0
  22. data/lib/markdown/merge/file_analysis.rb +353 -0
  23. data/lib/markdown/merge/file_analysis_base.rb +629 -0
  24. data/lib/markdown/merge/freeze_node.rb +93 -0
  25. data/lib/markdown/merge/gap_line_node.rb +136 -0
  26. data/lib/markdown/merge/link_definition_formatter.rb +49 -0
  27. data/lib/markdown/merge/link_definition_node.rb +157 -0
  28. data/lib/markdown/merge/link_parser.rb +421 -0
  29. data/lib/markdown/merge/link_reference_rehydrator.rb +320 -0
  30. data/lib/markdown/merge/markdown_structure.rb +123 -0
  31. data/lib/markdown/merge/merge_result.rb +166 -0
  32. data/lib/markdown/merge/node_type_normalizer.rb +126 -0
  33. data/lib/markdown/merge/output_builder.rb +166 -0
  34. data/lib/markdown/merge/partial_template_merger.rb +334 -0
  35. data/lib/markdown/merge/smart_merger.rb +221 -0
  36. data/lib/markdown/merge/smart_merger_base.rb +621 -0
  37. data/lib/markdown/merge/table_match_algorithm.rb +504 -0
  38. data/lib/markdown/merge/table_match_refiner.rb +136 -0
  39. data/lib/markdown/merge/version.rb +12 -0
  40. data/lib/markdown/merge/whitespace_normalizer.rb +251 -0
  41. data/lib/markdown/merge.rb +149 -0
  42. data/lib/markdown-merge.rb +4 -0
  43. data/sig/markdown/merge.rbs +341 -0
  44. data.tar.gz.sig +0 -0
  45. metadata +365 -0
  46. metadata.gz.sig +0 -0
data/README.md ADDED
@@ -0,0 +1,1087 @@
1
+ | 📍 NOTE |
2
+ | --- |
3
+ | RubyGems (the [GitHub org][rubygems-org], not the website) [suffered][draper-security] a [hostile takeover][ellen-takeover] in September 2025. |
4
+ | Ultimately [4 maintainers][simi-removed] were [hard removed][martin-removed] and a reason has been given for only 1 of those, while 2 others resigned in protest. |
5
+ | It is a [complicated story][draper-takeover] which is difficult to [parse quickly][draper-lies]. |
6
+ | Simply put - there was active policy for adding or removing maintainers/owners of [rubygems][rubygems-maint-policy] and [bundler][bundler-maint-policy], and those [policies were not followed][policy-fail]. |
7
+ | I'm adding notes like this to gems because I [don't condone theft][draper-theft] of repositories or gems from their rightful owners. |
8
+ | If a similar theft happened with my repos/gems, I'd hope some would stand up for me. |
9
+ | Disenfranchised former-maintainers have started [gem.coop][gem-coop]. |
10
+ | Once available I will publish there exclusively; unless RubyCentral makes amends with the community. |
11
+ | The ["Technology for Humans: Joel Draper"][reinteractive-podcast] podcast episode by [reinteractive][reinteractive] is the most cogent summary I'm aware of. |
12
+ | See [here][gem-naming], [here][gem-coop] and [here][martin-ann] for more info on what comes next. |
13
+ | What I'm doing: A (WIP) proposal for [bundler/gem scopes][gem-scopes], and a (WIP) proposal for a federated [gem server][gem-server]. |
14
+
15
+ [rubygems-org]: https://github.com/rubygems/
16
+ [draper-security]: https://joel.drapper.me/p/ruby-central-security-measures/
17
+ [draper-takeover]: https://joel.drapper.me/p/ruby-central-takeover/
18
+ [ellen-takeover]: https://pup-e.com/blog/goodbye-rubygems/
19
+ [simi-removed]: https://www.reddit.com/r/ruby/s/gOk42POCaV
20
+ [martin-removed]: https://bsky.app/profile/martinemde.com/post/3m3occezxxs2q
21
+ [draper-lies]: https://joel.drapper.me/p/ruby-central-fact-check/
22
+ [draper-theft]: https://joel.drapper.me/p/ruby-central/
23
+ [reinteractive]: https://reinteractive.com/ruby-on-rails
24
+ [gem-coop]: https://gem.coop
25
+ [gem-naming]: https://github.com/gem-coop/gem.coop/issues/12
26
+ [martin-ann]: https://martinemde.com/2025/10/05/announcing-gem-coop.html
27
+ [gem-scopes]: https://github.com/galtzo-floss/bundle-namespace
28
+ [gem-server]: https://github.com/galtzo-floss/gem-server
29
+ [reinteractive-podcast]: https://youtu.be/_H4qbtC5qzU?si=BvuBU90R2wAqD2E6
30
+ [bundler-maint-policy]: https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/bundler/playbooks/TEAM_CHANGES.md
31
+ [rubygems-maint-policy]: https://github.com/ruby/rubygems/blob/b1ab33a3d52310a84d16b193991af07f5a6a07c0/doc/rubygems/POLICIES.md?plain=1#L187-L196
32
+ [policy-fail]: https://www.reddit.com/r/ruby/comments/1ove9vp/rubycentral_hates_this_one_fact/
33
+
34
+ [![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]][🖼️galtzo-discord] [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]][🖼️ruby-lang] [![kettle-rb Logo by Aboling0, CC BY-SA 4.0][🖼️kettle-rb-i]][🖼️kettle-rb]
35
+
36
+ [🖼️galtzo-i]: https://logos.galtzo.com/assets/images/galtzo-floss/avatar-192px.svg
37
+ [🖼️galtzo-discord]: https://discord.gg/3qme4XHNKN
38
+ [🖼️ruby-lang-i]: https://logos.galtzo.com/assets/images/ruby-lang/avatar-192px.svg
39
+ [🖼️ruby-lang]: https://www.ruby-lang.org/
40
+ [🖼️kettle-rb-i]: https://logos.galtzo.com/assets/images/kettle-rb/avatar-192px.svg
41
+ [🖼️kettle-rb]: https://github.com/kettle-rb
42
+
43
+ # ☯️ Markdown::Merge
44
+
45
+ [![Version][👽versioni]][👽dl-rank] [![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
+
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
+
49
+ -----
50
+
51
+ `if ci_badges.map(&:color).all? { it == "green"}` 👇️ send money so I can do more of this. FLOSS maintenance is now my full-time job.
52
+
53
+ [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]
54
+
55
+ ## 🌻 Synopsis
56
+
57
+ Markdown::Merge provides **intelligent Markdown file merging** using tree\_haver backends. It can be used standalone or through parser-specific wrappers.
58
+
59
+ **Direct usage** (with auto-detected or specified backend):
60
+
61
+ ```ruby
62
+ require "markdown/merge"
63
+
64
+ # Auto-detect available backend (commonmarker or markly)
65
+ merger = Markdown::Merge::SmartMerger.new(template_content, dest_content)
66
+ result = merger.merge
67
+
68
+ # Or specify a backend explicitly
69
+ merger = Markdown::Merge::SmartMerger.new(template_content, dest_content, backend: :markly)
70
+ ```
71
+
72
+ **Via parser-specific wrappers** (for hard dependencies and backend-specific defaults):
73
+
74
+ - [commonmarker-merge][commonmarker-merge] - Uses Comrak (Rust) via Commonmarker
75
+ - [markly-merge][markly-merge] - Uses libcmark-gfm (C) via Markly
76
+
77
+ ### Key Features
78
+
79
+ - **Multiple Backends**: Supports Commonmarker and Markly through tree\_haver's unified API
80
+ - **Type Normalization**: Canonical node types (`:heading`, `:paragraph`, etc.) work across all backends
81
+ - **Extensible**: Register custom backends via `NodeTypeNormalizer.register_backend`
82
+ - **Structure-Aware**: Understands headings, paragraphs, lists, code blocks, tables, and other block elements
83
+ - **Freeze Block Support**: Respects freeze markers (default: `markdown-merge:freeze` / `markdown-merge:unfreeze`) for template merge control - customizable to match your project's conventions
84
+ - **Inner-Merge Code Blocks**: Optionally merge fenced code blocks using language-specific mergers (Ruby → prism-merge, YAML → psych-merge, JSON → json-merge, TOML → toml-merge)
85
+ - **Table Match Refiner**: Fuzzy matching algorithm for tables with similar but not identical headers
86
+ - **Full Provenance**: Tracks origin of every node
87
+ - **Customizable**:
88
+ - `backend` - select `:commonmarker`, `:markly`, or `:auto`
89
+ - `signature_generator` - callable custom signature generators
90
+ - `preference` - setting of `:template`, `:destination`, or a Hash for per-node-type preferences
91
+ - `add_template_only_nodes` - setting to retain sections that do not exist in destination
92
+ - `freeze_token` - customize freeze block markers (default: `"markdown-merge"`)
93
+ - `inner_merge_code_blocks` - enable language-aware code block merging
94
+ - `match_refiner` - fuzzy matching for unmatched nodes (e.g., `TableMatchRefiner`)
95
+
96
+ ### Supported Node Types
97
+
98
+ Signatures computed by default for common Markdown block elements:
99
+
100
+ | Node Type | Signature Format | Matching Behavior |
101
+ | --- | --- | --- |
102
+ | Heading | `[:heading, level, text]` | Headings match by level and text content |
103
+ | Paragraph | `[:paragraph, content_hash]` | Paragraphs match by content hash |
104
+ | List | `[:list, type, item_count]` | Lists match by type (bullet/ordered) and item count |
105
+ | Code Block | `[:code_block, language, content_hash]` | Code blocks match by language and content |
106
+ | Block Quote | `[:blockquote, content_hash]` | Block quotes match by content hash |
107
+ | Table | `[:table, row_count, header_hash]` | Tables match by structure and header content |
108
+ | HTML Block | `[:html, content_hash]` | HTML blocks match by content hash |
109
+ | Thematic Break | `[:hrule]` | Horizontal rules always match |
110
+ | Footnote Definition | `[:footnote_definition, label]` | Footnotes match by label/name |
111
+
112
+ ### The `*-merge` Gem Family
113
+
114
+ 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.
115
+
116
+ | Gem | Version | CI | | Language<br>/ Format | Parser Backend(s) | Description |
117
+ |------------------------------------------|----------------------------------------------------------------|--------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|-------------|
118
+ | [tree_haver][tree_haver] | [![Version][tree_haver-gem-i]][tree_haver-gem] | [![Version][tree_haver-ci-i]][tree_haver-ci] | Multi | MRI C, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, Citrus, Parslet | **Foundation**: Cross-Ruby adapter for parsing libraries (like Faraday for HTTP) |
119
+ | [ast-merge][ast-merge] | [![Version][ast-merge-gem-i]][ast-merge-gem] | [![Version][ast-merge-ci-i]][ast-merge-ci] | Text | internal | **Infrastructure**: Shared base classes and merge logic for all `*-merge` gems |
120
+ | [bash-merge][bash-merge] | [![Version][bash-merge-gem-i]][bash-merge-gem] | [![Version][bash-merge-ci-i]][bash-merge-ci] | Bash | [tree-sitter-bash][ts-bash] (via tree_haver) | Smart merge for Bash scripts |
121
+ | [commonmarker-merge][commonmarker-merge] | [![Version][commonmarker-merge-gem-i]][commonmarker-merge-gem] | [![Version][commonmarker-merge-ci-i]][commonmarker-merge-ci] | Markdown | [Commonmarker][commonmarker] (via tree_haver) | Smart merge for Markdown (CommonMark via comrak Rust) |
122
+ | [dotenv-merge][dotenv-merge] | [![Version][dotenv-merge-gem-i]][dotenv-merge-gem] | [![Version][dotenv-merge-ci-i]][dotenv-merge-ci] | Dotenv | internal | Smart merge for `.env` files |
123
+ | [json-merge][json-merge] | [![Version][json-merge-gem-i]][json-merge-gem] | [![Version][json-merge-ci-i]][json-merge-ci] | JSON | [tree-sitter-json][ts-json] (via tree_haver) | Smart merge for JSON files |
124
+ | [jsonc-merge][jsonc-merge] | [![Version][jsonc-merge-gem-i]][jsonc-merge-gem] | [![Version][jsonc-merge-ci-i]][jsonc-merge-ci] | JSONC | [tree-sitter-jsonc][ts-jsonc] (via tree_haver) | ⚠️ Proof of concept; Smart merge for JSON with Comments |
125
+ | [markdown-merge][markdown-merge] | [![Version][markdown-merge-gem-i]][markdown-merge-gem] | [![Version][markdown-merge-ci-i]][markdown-merge-ci] | Markdown | [Commonmarker][commonmarker] / [Markly][markly] (via tree_haver) | **Foundation**: Shared base for Markdown mergers with inner code block merging |
126
+ | [markly-merge][markly-merge] | [![Version][markly-merge-gem-i]][markly-merge-gem] | [![Version][markly-merge-ci-i]][markly-merge-ci] | Markdown | [Markly][markly] (via tree_haver) | Smart merge for Markdown (CommonMark via cmark-gfm C) |
127
+ | [prism-merge][prism-merge] | [![Version][prism-merge-gem-i]][prism-merge-gem] | [![Version][prism-merge-ci-i]][prism-merge-ci] | Ruby | [Prism][prism] (`prism` std lib gem) | Smart merge for Ruby source files |
128
+ | [psych-merge][psych-merge] | [![Version][psych-merge-gem-i]][psych-merge-gem] | [![Version][psych-merge-ci-i]][psych-merge-ci] | YAML | [Psych][psych] (`psych` std lib gem) | Smart merge for YAML files |
129
+ | [rbs-merge][rbs-merge] | [![Version][rbs-merge-gem-i]][rbs-merge-gem] | [![Version][rbs-merge-ci-i]][rbs-merge-ci] | RBS | [tree-sitter-bash][ts-rbs] (via tree_haver), [RBS][rbs] (`rbs` std lib gem) | Smart merge for Ruby type signatures |
130
+ | [toml-merge][toml-merge] | [![Version][toml-merge-gem-i]][toml-merge-gem] | [![Version][toml-merge-ci-i]][toml-merge-ci] | TOML | [Parslet + toml][toml], [Citrus + toml-rb][toml-rb], [tree-sitter-toml][ts-toml] (all via tree_haver) | Smart merge for TOML files |
131
+
132
+ #### Backend Platform Compatibility
133
+
134
+ tree_haver supports multiple parsing backends, but not all backends work on all Ruby platforms:
135
+
136
+ | Platform 👉️<br> TreeHaver Backend 👇️ | MRI | JRuby | TruffleRuby | Notes |
137
+ |------------------------------------------------|:---:|:-----:|:-----------:|-----------------------------------------------------|
138
+ | **MRI** ([ruby_tree_sitter][ruby_tree_sitter]) | ✅ | ❌ | ❌ | C extension, MRI only |
139
+ | **Rust** ([tree_stump][tree_stump]) | ✅ | ❌ | ❌ | Rust extension via magnus/rb-sys, MRI only |
140
+ | **FFI** | ✅ | ✅ | ❌ | TruffleRuby's FFI doesn't support `STRUCT_BY_VALUE` |
141
+ | **Java** ([jtreesitter][jtreesitter]) | ❌ | ✅ | ❌ | JRuby only, requires grammar JARs |
142
+ | **Prism** | ✅ | ✅ | ✅ | Ruby parsing, stdlib in Ruby 3.4+ |
143
+ | **Psych** | ✅ | ✅ | ✅ | YAML parsing, stdlib |
144
+ | **Citrus** | ✅ | ✅ | ✅ | Pure Ruby PEG parser, no native dependencies |
145
+ | **Parslet** | ✅ | ✅ | ✅ | Pure Ruby PEG parser, no native dependencies |
146
+ | **Commonmarker** | ✅ | ❌ | ❓ | Rust extension for Markdown |
147
+ | **Markly** | ✅ | ❌ | ❓ | C extension for Markdown |
148
+
149
+ **Legend**: ✅ = Works, ❌ = Does not work, ❓ = Untested
150
+
151
+ **Why some backends don't work on certain platforms**:
152
+
153
+ - **JRuby**: Runs on the JVM; cannot load native C/Rust extensions (`.so` files)
154
+ - **TruffleRuby**: Has C API emulation via Sulong/LLVM, but it doesn't expose all MRI internals that native extensions require (e.g., `RBasic.flags`, `rb_gc_writebarrier`)
155
+ - **FFI on TruffleRuby**: TruffleRuby's FFI implementation doesn't support returning structs by value, which tree-sitter's C API requires
156
+
157
+ **Example implementations** for the gem templating use case:
158
+
159
+ | Gem | Purpose | Description |
160
+ |--------------------------|-----------------|-----------------------------------------------|
161
+ | [kettle-dev][kettle-dev] | Gem Development | Gem templating tool using `*-merge` gems |
162
+ | [kettle-jem][kettle-jem] | Gem Templating | Gem template library with smart merge support |
163
+
164
+ [tree_haver]: https://github.com/kettle-rb/tree_haver
165
+ [ast-merge]: https://github.com/kettle-rb/ast-merge
166
+ [prism-merge]: https://github.com/kettle-rb/prism-merge
167
+ [psych-merge]: https://github.com/kettle-rb/psych-merge
168
+ [json-merge]: https://github.com/kettle-rb/json-merge
169
+ [jsonc-merge]: https://github.com/kettle-rb/jsonc-merge
170
+ [bash-merge]: https://github.com/kettle-rb/bash-merge
171
+ [rbs-merge]: https://github.com/kettle-rb/rbs-merge
172
+ [dotenv-merge]: https://github.com/kettle-rb/dotenv-merge
173
+ [toml-merge]: https://github.com/kettle-rb/toml-merge
174
+ [markdown-merge]: https://github.com/kettle-rb/markdown-merge
175
+ [markly-merge]: https://github.com/kettle-rb/markly-merge
176
+ [commonmarker-merge]: https://github.com/kettle-rb/commonmarker-merge
177
+ [kettle-dev]: https://github.com/kettle-rb/kettle-dev
178
+ [kettle-jem]: https://github.com/kettle-rb/kettle-jem
179
+ [tree_haver-gem]: https://bestgems.org/gems/tree_haver
180
+ [ast-merge-gem]: https://bestgems.org/gems/ast-merge
181
+ [prism-merge-gem]: https://bestgems.org/gems/prism-merge
182
+ [psych-merge-gem]: https://bestgems.org/gems/psych-merge
183
+ [json-merge-gem]: https://bestgems.org/gems/json-merge
184
+ [jsonc-merge-gem]: https://bestgems.org/gems/jsonc-merge
185
+ [bash-merge-gem]: https://bestgems.org/gems/bash-merge
186
+ [rbs-merge-gem]: https://bestgems.org/gems/rbs-merge
187
+ [dotenv-merge-gem]: https://bestgems.org/gems/dotenv-merge
188
+ [toml-merge-gem]: https://bestgems.org/gems/toml-merge
189
+ [markdown-merge-gem]: https://bestgems.org/gems/markdown-merge
190
+ [markly-merge-gem]: https://bestgems.org/gems/markly-merge
191
+ [commonmarker-merge-gem]: https://bestgems.org/gems/commonmarker-merge
192
+ [kettle-dev-gem]: https://bestgems.org/gems/kettle-dev
193
+ [kettle-jem-gem]: https://bestgems.org/gems/kettle-jem
194
+ [tree_haver-gem-i]: https://img.shields.io/gem/v/tree_haver.svg
195
+ [ast-merge-gem-i]: https://img.shields.io/gem/v/ast-merge.svg
196
+ [prism-merge-gem-i]: https://img.shields.io/gem/v/prism-merge.svg
197
+ [psych-merge-gem-i]: https://img.shields.io/gem/v/psych-merge.svg
198
+ [json-merge-gem-i]: https://img.shields.io/gem/v/json-merge.svg
199
+ [jsonc-merge-gem-i]: https://img.shields.io/gem/v/jsonc-merge.svg
200
+ [bash-merge-gem-i]: https://img.shields.io/gem/v/bash-merge.svg
201
+ [rbs-merge-gem-i]: https://img.shields.io/gem/v/rbs-merge.svg
202
+ [dotenv-merge-gem-i]: https://img.shields.io/gem/v/dotenv-merge.svg
203
+ [toml-merge-gem-i]: https://img.shields.io/gem/v/toml-merge.svg
204
+ [markdown-merge-gem-i]: https://img.shields.io/gem/v/markdown-merge.svg
205
+ [markly-merge-gem-i]: https://img.shields.io/gem/v/markly-merge.svg
206
+ [commonmarker-merge-gem-i]: https://img.shields.io/gem/v/commonmarker-merge.svg
207
+ [kettle-dev-gem-i]: https://img.shields.io/gem/v/kettle-dev.svg
208
+ [kettle-jem-gem-i]: https://img.shields.io/gem/v/kettle-jem.svg
209
+ [tree_haver-ci-i]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml/badge.svg
210
+ [ast-merge-ci-i]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml/badge.svg
211
+ [prism-merge-ci-i]: https://github.com/kettle-rb/prism-merge/actions/workflows/current.yml/badge.svg
212
+ [psych-merge-ci-i]: https://github.com/kettle-rb/psych-merge/actions/workflows/current.yml/badge.svg
213
+ [json-merge-ci-i]: https://github.com/kettle-rb/json-merge/actions/workflows/current.yml/badge.svg
214
+ [jsonc-merge-ci-i]: https://github.com/kettle-rb/jsonc-merge/actions/workflows/current.yml/badge.svg
215
+ [bash-merge-ci-i]: https://github.com/kettle-rb/bash-merge/actions/workflows/current.yml/badge.svg
216
+ [rbs-merge-ci-i]: https://github.com/kettle-rb/rbs-merge/actions/workflows/current.yml/badge.svg
217
+ [dotenv-merge-ci-i]: https://github.com/kettle-rb/dotenv-merge/actions/workflows/current.yml/badge.svg
218
+ [toml-merge-ci-i]: https://github.com/kettle-rb/toml-merge/actions/workflows/current.yml/badge.svg
219
+ [markdown-merge-ci-i]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml/badge.svg
220
+ [markly-merge-ci-i]: https://github.com/kettle-rb/markly-merge/actions/workflows/current.yml/badge.svg
221
+ [commonmarker-merge-ci-i]: https://github.com/kettle-rb/commonmarker-merge/actions/workflows/current.yml/badge.svg
222
+ [kettle-dev-ci-i]: https://github.com/kettle-rb/kettle-dev/actions/workflows/current.yml/badge.svg
223
+ [kettle-jem-ci-i]: https://github.com/kettle-rb/kettle-jem/actions/workflows/current.yml/badge.svg
224
+ [tree_haver-ci]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml
225
+ [ast-merge-ci]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml
226
+ [prism-merge-ci]: https://github.com/kettle-rb/prism-merge/actions/workflows/current.yml
227
+ [psych-merge-ci]: https://github.com/kettle-rb/psych-merge/actions/workflows/current.yml
228
+ [json-merge-ci]: https://github.com/kettle-rb/json-merge/actions/workflows/current.yml
229
+ [jsonc-merge-ci]: https://github.com/kettle-rb/jsonc-merge/actions/workflows/current.yml
230
+ [bash-merge-ci]: https://github.com/kettle-rb/bash-merge/actions/workflows/current.yml
231
+ [rbs-merge-ci]: https://github.com/kettle-rb/rbs-merge/actions/workflows/current.yml
232
+ [dotenv-merge-ci]: https://github.com/kettle-rb/dotenv-merge/actions/workflows/current.yml
233
+ [toml-merge-ci]: https://github.com/kettle-rb/toml-merge/actions/workflows/current.yml
234
+ [markdown-merge-ci]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml
235
+ [markly-merge-ci]: https://github.com/kettle-rb/markly-merge/actions/workflows/current.yml
236
+ [commonmarker-merge-ci]: https://github.com/kettle-rb/commonmarker-merge/actions/workflows/current.yml
237
+ [kettle-dev-ci]: https://github.com/kettle-rb/kettle-dev/actions/workflows/current.yml
238
+ [kettle-jem-ci]: https://github.com/kettle-rb/kettle-jem/actions/workflows/current.yml
239
+ [prism]: https://github.com/ruby/prism
240
+ [psych]: https://github.com/ruby/psych
241
+ [ts-json]: https://github.com/tree-sitter/tree-sitter-json
242
+ [ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc
243
+ [ts-bash]: https://github.com/tree-sitter/tree-sitter-bash
244
+ [ts-rbs]: https://github.com/joker1007/tree-sitter-rbs
245
+ [ts-toml]: https://github.com/tree-sitter-grammars/tree-sitter-toml
246
+ [dotenv]: https://github.com/bkeepers/dotenv
247
+ [rbs]: https://github.com/ruby/rbs
248
+ [toml-rb]: https://github.com/emancu/toml-rb
249
+ [toml]: https://github.com/jm/toml
250
+ [markly]: https://github.com/ioquatix/markly
251
+ [commonmarker]: https://github.com/gjtorikian/commonmarker
252
+ [ruby_tree_sitter]: https://github.com/Faveod/ruby-tree-sitter
253
+ [tree_stump]: https://github.com/joker1007/tree_stump
254
+ [jtreesitter]: https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
255
+
256
+ ## 💡 Info you can shake a stick at
257
+
258
+ | Tokens to Remember | [![Gem name][⛳️name-img]][👽dl-rank] [![Gem namespace][⛳️namespace-img]][📜src-gh] |
259
+ | --- | --- |
260
+ | Works with JRuby | [![JRuby 10.0 Compat][💎jruby-c-i]][🚎11-c-wf] [![JRuby HEAD Compat][💎jruby-headi]][🚎3-hd-wf] |
261
+ | Works with Truffle Ruby | [![Truffle Ruby 23.1 Compat][💎truby-23.1i]][🚎9-t-wf] [![Truffle Ruby 24.1 Compat][💎truby-c-i]][🚎11-c-wf] |
262
+ | Works with MRI Ruby 3 | [![Ruby 3.2 Compat][💎ruby-3.2i]][🚎6-s-wf] [![Ruby 3.3 Compat][💎ruby-3.3i]][🚎6-s-wf] [![Ruby 3.4 Compat][💎ruby-c-i]][🚎11-c-wf] [![Ruby HEAD Compat][💎ruby-headi]][🚎3-hd-wf] |
263
+ | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] |
264
+ | Source | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on Github.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ\!][🧮kloc-img]][🧮kloc] |
265
+ | Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]][🚎yard-current] [![YARD on Galtzo.com][📜docs-head-rd-img]][🚎yard-head] [![Maintainer Blog][🚂maint-blog-img]][🚂maint-blog] [![GitLab Wiki][📜gl-wiki-img]][📜gl-wiki] [![GitHub Wiki][📜gh-wiki-img]][📜gh-wiki] |
266
+ | Compliance | [![License: MIT][📄license-img]][📄license-ref] [![Compatible with Apache Software Projects: Verified by SkyWalking Eyes][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img][📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |
267
+ | Style | [![Enforced Code Style Linter][💎rlts-img]][💎rlts] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] [![Gitmoji Commits][📌gitmoji-img]][📌gitmoji] [![Compatibility appraised by: appraisal2][💎appraisal2-img]][💎appraisal2] |
268
+ | Maintainer 🎖️ | [![Follow Me on LinkedIn][💖🖇linkedin-img]][💖🖇linkedin] [![Follow Me on Ruby.Social][💖🐘ruby-mast-img]][💖🐘ruby-mast] [![Follow Me on Bluesky][💖🦋bluesky-img]][💖🦋bluesky] [![Contact Maintainer][🚂maint-contact-img]][🚂maint-contact] [![My technical writing][💖💁🏼‍♂️devto-img]][💖💁🏼‍♂️devto] |
269
+ | `...` 💖 | [![Find Me on WellFound:][💖✌️wellfound-img]][💖✌️wellfound] [![Find Me on CrunchBase][💖💲crunchbase-img]][💖💲crunchbase] [![My LinkTree][💖🌳linktree-img]][💖🌳linktree] [![More About Me][💖💁🏼‍♂️aboutme-img]][💖💁🏼‍♂️aboutme] [🧊][💖🧊berg] [🐙][💖🐙hub] [🛖][💖🛖hut] [🧪][💖🧪lab] |
270
+
271
+ ### Compatibility
272
+
273
+ Compatible with MRI Ruby 3.2.0+, and concordant releases of JRuby, and TruffleRuby.
274
+
275
+ | 🚚 *Amazing* test matrix was brought to you by | 🔎 appraisal2 🔎 and the color 💚 green 💚 |
276
+ | --- | --- |
277
+ | 👟 Check it out\! | ✨ [github.com/appraisal-rb/appraisal2][💎appraisal2] ✨ |
278
+
279
+ ### Federated DVCS
280
+
281
+ <details markdown="1">
282
+ <summary>Find this repo on federated forges (Coming soon!)</summary>
283
+
284
+ | Federated [DVCS][💎d-in-dvcs] Repository | Status | Issues | PRs | Wiki | CI | Discussions |
285
+ | --- | --- | --- | --- | --- | --- | --- |
286
+ | 🧪 [kettle-rb/markdown-merge on GitLab][📜src-gl] | The Truth | [💚][🤝gl-issues] | [💚][🤝gl-pulls] | [💚][📜gl-wiki] | 🐭 Tiny Matrix | ➖ |
287
+ | 🧊 [kettle-rb/markdown-merge on CodeBerg][📜src-cb] | An Ethical Mirror ([Donate][🤝cb-donate]) | [💚][🤝cb-issues] | [💚][🤝cb-pulls] | ➖ | ⭕️ No Matrix | ➖ |
288
+ | 🐙 [kettle-rb/markdown-merge on GitHub][📜src-gh] | Another Mirror | [💚][🤝gh-issues] | [💚][🤝gh-pulls] | [💚][📜gh-wiki] | 💯 Full Matrix | [💚][gh-discussions] |
289
+ | 🎮️ [Discord Server][🖼️galtzo-discord] | [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord] | [Let's][🖼️galtzo-discord] | [talk][🖼️galtzo-discord] | [about][🖼️galtzo-discord] | [this][🖼️galtzo-discord] | [library\!][🖼️galtzo-discord] |
290
+
291
+ </details>
292
+
293
+ [gh-discussions]: https://github.com/kettle-rb/markdown-merge/discussions
294
+
295
+ ### Enterprise Support [![Tidelift](https://tidelift.com/badges/package/rubygems/markdown-merge)][🏙️entsup-tidelift]
296
+
297
+ Available as part of the Tidelift Subscription.
298
+
299
+ <details markdown="1">
300
+ <summary>Need enterprise-level guarantees?</summary>
301
+
302
+ The maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.
303
+
304
+ [![Get help from me on Tidelift][🏙️entsup-tidelift-img]][🏙️entsup-tidelift]
305
+
306
+ - 💡Subscribe for support guarantees covering *all* your FLOSS dependencies
307
+ - 💡Tidelift is part of [Sonar][🏙️entsup-tidelift-sonar]
308
+ - 💡Tidelift pays maintainers to maintain the software you depend on\!<br/>📊`@`Pointy Haired Boss: An [enterprise support][🏙️entsup-tidelift] subscription is "[never gonna let you down][🧮kloc]", and *supports* open source maintainers
309
+ Alternatively:
310
+ - [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]
311
+ - [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork]
312
+ - [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor]
313
+
314
+ </details>
315
+
316
+ ## ✨ Installation
317
+
318
+ Install the gem and add to the application's Gemfile by executing:
319
+
320
+ ```console
321
+ bundle add markdown-merge
322
+ ```
323
+
324
+ If bundler is not being used to manage dependencies, install the gem by executing:
325
+
326
+ ```console
327
+ gem install markdown-merge
328
+ ```
329
+
330
+ ### 🔒 Secure Installation
331
+
332
+ <details markdown="1">
333
+ <summary>For Medium or High Security Installations</summary>
334
+
335
+ This gem is cryptographically signed, and has verifiable [SHA-256 and SHA-512][💎SHA_checksums] checksums by
336
+ [stone\_checksums][💎stone_checksums]. Be sure the gem you install hasn’t been tampered with
337
+ by following the instructions below.
338
+
339
+ Add my public key (if you haven’t already, expires 2045-04-29) as a trusted certificate:
340
+
341
+ ```console
342
+ gem cert --add <(curl -Ls https://raw.github.com/galtzo-floss/certs/main/pboling.pem)
343
+ ```
344
+
345
+ You only need to do that once. Then proceed to install with:
346
+
347
+ ```console
348
+ gem install markdown-merge -P HighSecurity
349
+ ```
350
+
351
+ The `HighSecurity` trust profile will verify signed gems, and not allow the installation of unsigned dependencies.
352
+
353
+ If you want to up your security game full-time:
354
+
355
+ ```console
356
+ bundle config set --global trust-policy MediumSecurity
357
+ ```
358
+
359
+ `MediumSecurity` instead of `HighSecurity` is necessary if not all the gems you use are signed.
360
+
361
+ NOTE: Be prepared to track down certs for signed gems and add them the same way you added mine.
362
+
363
+ </details>
364
+
365
+ ## ⚙️ Configuration
366
+
367
+ ### SmartMerger Configuration
368
+
369
+ The `SmartMerger` class is the main entry point for merging Markdown files:
370
+
371
+ ```ruby
372
+ require "markdown/merge"
373
+
374
+ merger = Markdown::Merge::SmartMerger.new(
375
+ template_content,
376
+ dest_content,
377
+
378
+ # Backend selection (default: :auto)
379
+ # :auto - auto-detect available backend (tries commonmarker first, then markly)
380
+ # :commonmarker - use Commonmarker (comrak Rust parser)
381
+ # :markly - use Markly (cmark-gfm C library)
382
+ backend: :auto,
383
+
384
+ # Which version to prefer when nodes match but differ
385
+ # :destination (default) - keep destination content (preserves customizations)
386
+ # :template - use template content (applies updates)
387
+ preference: :destination,
388
+
389
+ # Whether to add template-only nodes to the result
390
+ # false (default) - only include sections that exist in destination
391
+ # true - include all template sections
392
+ add_template_only_nodes: false,
393
+
394
+ # Token for freeze block markers
395
+ # Default: "markdown-merge"
396
+ # Looks for: <!-- markdown-merge:freeze --> / <!-- markdown-merge:unfreeze -->
397
+ freeze_token: "markdown-merge",
398
+
399
+ # Enable inner-merge for fenced code blocks
400
+ # false (default) - use standard conflict resolution for code blocks
401
+ # true - merge code block contents using language-specific mergers
402
+ # CodeBlockMerger instance - use custom CodeBlockMerger
403
+ inner_merge_code_blocks: false,
404
+
405
+ # Match refiner for fuzzy matching of unmatched nodes
406
+ # nil (default) - exact matching only
407
+ # TableMatchRefiner.new - enable fuzzy table matching
408
+ match_refiner: nil,
409
+
410
+ # Custom signature generator (optional)
411
+ # Receives a node (wrapped with canonical merge_type), returns a signature array or nil
412
+ # Return the node itself to fall through to default signature
413
+ signature_generator: nil,
414
+
415
+ # Backend-specific options (passed through to parser)
416
+ # For commonmarker: options: {}
417
+ # For markly: flags: Markly::DEFAULT, extensions: [:table]
418
+ )
419
+ ```
420
+
421
+ ### Text Matching Behavior
422
+
423
+ **Important**: When matching nodes by text content (such as for anchor patterns in
424
+ `PartialTemplateMerger`), the `.text` method returns **plain text without markdown formatting**.
425
+
426
+ This means:
427
+
428
+ - Markdown: `` ### The `*-merge` Gem Family ``
429
+ - `.text` returns: `"The *-merge Gem Family\n"`
430
+
431
+ The backticks around `*-merge` are stripped because they are inline formatting, not content.
432
+ This is true for both Commonmarker and Markly backends.
433
+
434
+ **Anchor pattern examples**:
435
+
436
+ ```ruby
437
+ # ❌ WRONG - backticks are stripped, so this won't match
438
+ anchor: { type: :heading, text: /`\*-merge` Gem Family/ }
439
+
440
+ # ✅ CORRECT - match the plain text content
441
+ anchor: { type: :heading, text: /\*-merge.*Gem Family/ }
442
+
443
+ # ✅ CORRECT - use beginning anchor for exact heading match
444
+ anchor: { type: :heading, text: /^The \*-merge Gem Family/ }
445
+ ```
446
+
447
+ **Other markdown formatting that is stripped from `.text`**:
448
+
449
+ - Bold: `**text**` → `text`
450
+ - Italic: `*text*` or `_text_` → `text`
451
+ - Code: `` `code` `` → `code`
452
+ - Links: `[text](url)` → `text`
453
+ - Images: `![alt](src)` → `alt`
454
+
455
+ **Note**: Different parsers may have other idiosyncrasies. For example:
456
+
457
+ - Trailing newlines may or may not be present
458
+ - Whitespace normalization may differ
459
+ - Entity encoding may vary
460
+
461
+ Always test your patterns against actual parsed content when building merge recipes.
462
+
463
+ ### Node Type Normalization
464
+
465
+ markdown-merge normalizes node types across backends so merge rules are portable:
466
+
467
+ ```ruby
468
+ # These are equivalent regardless of backend
469
+ # Markly's :header becomes :heading
470
+ # Markly's :hrule becomes :thematic_break
471
+ # etc.
472
+
473
+ # Register a custom backend's type mappings
474
+ Markdown::Merge::NodeTypeNormalizer.register_backend(:my_parser, {
475
+ h1: :heading,
476
+ h2: :heading,
477
+ para: :paragraph,
478
+ # ...
479
+ })
480
+ ```
481
+
482
+ ### Parser-Specific Wrappers
483
+
484
+ For convenience, parser-specific wrappers provide backend-specific defaults:
485
+
486
+ ```ruby
487
+ # commonmarker-merge (freeze_token: "commonmarker-merge", inner_merge_code_blocks: false)
488
+ require "commonmarker/merge"
489
+ merger = Commonmarker::Merge::SmartMerger.new(template, dest, options: {})
490
+
491
+ # markly-merge (freeze_token: "markly-merge", inner_merge_code_blocks: true)
492
+ require "markly/merge"
493
+ merger = Markly::Merge::SmartMerger.new(template, dest, flags: Markly::DEFAULT, extensions: [:table])
494
+ ```
495
+
496
+ ````
497
+ ### Freeze Blocks
498
+
499
+ Freeze blocks protect sections from being modified during merges. They are marked
500
+ with HTML comments that are invisible when the Markdown is rendered:
501
+
502
+ ```markdown
503
+ <!-- markdown-merge:freeze -->
504
+ ## This Section Is Protected
505
+
506
+ Any content here will be preserved exactly as-is during merges.
507
+ The merge tool will not modify, replace, or remove this content.
508
+
509
+ <!-- markdown-merge:unfreeze -->
510
+ ````
511
+
512
+ Add an optional frozen reason to document why:
513
+
514
+ ```markdown
515
+ <!-- markdown-merge:freeze Custom table - manually maintained -->
516
+ | Feature | Status |
517
+ |---------|--------|
518
+ | Custom | ✅ |
519
+ <!-- markdown-merge:unfreeze -->
520
+ ```
521
+
522
+ ### Inner-Merge Code Blocks
523
+
524
+ When enabled, fenced code blocks are merged using language-specific `*-merge` gems:
525
+
526
+ ```ruby
527
+ merger = SomeParser::Merge::SmartMerger.new(
528
+ template,
529
+ destination,
530
+ inner_merge_code_blocks: true,
531
+ )
532
+ ```
533
+
534
+ Supported languages and their mergers:
535
+
536
+ | Language | Fence Info | Merger |
537
+ | --- | --- | --- |
538
+ | Ruby | `ruby`, `rb` | prism-merge |
539
+ | YAML | `yaml`, `yml` | psych-merge |
540
+ | JSON | `json` | json-merge |
541
+ | TOML | `toml` | toml-merge |
542
+
543
+ Example with a Ruby code block:
544
+
545
+ ````markdown
546
+ ```ruby
547
+ # Template
548
+ class MyClass
549
+ def new_method
550
+ puts "from template"
551
+ end
552
+ end)))))))))))
553
+ ```
554
+ ````
555
+
556
+ When merged(with:
557
+
558
+ ````markdown
559
+ ```ruby
560
+ # Destination
561
+ class MyClass
562
+ def existing_method
563
+ puts "custom"
564
+ end
565
+ end)
566
+ ```
567
+ ````
568
+
569
+ Result (with `inner_merge_code_blocks: true`):
570
+
571
+ ````markdown
572
+ ```ruby
573
+ class MyClass
574
+ def existing_method
575
+ puts "custom"
576
+ end
577
+
578
+ def new_method
579
+ puts "from template"
580
+ end
581
+ end
582
+ ```
583
+ ````
584
+
585
+ ### Table Match Refiner
586
+
587
+ When tables don't match by exact signature, the `TableMatchRefiner` uses
588
+ fuzzy matching to pair tables with similar structure:
589
+
590
+ ```ruby
591
+ refiner = Markdown::Merge::TableMatchRefiner.new(
592
+ threshold: 0.5, # Minimum similarity (0.0-1.0)
593
+ algorithm_options: {
594
+ weights: {
595
+ header_match: 0.25, # Header cell similarity
596
+ first_column: 0.20, # Row label similarity
597
+ row_content: 0.25, # Row content overlap
598
+ total_cells: 0.15, # Overall cell matching
599
+ position: 0.15, # Position distance
600
+ },
601
+ },
602
+ )
603
+
604
+ merger = SomeParser::Merge::SmartMerger.new(
605
+ template,
606
+ destination,
607
+ match_refiner: refiner,
608
+ )
609
+ ```
610
+
611
+ ### Debug Logging
612
+
613
+ Enable debug logging to see merge decisions:
614
+
615
+ ```bash
616
+ export MARKDOWN_MERGE_DEBUG=1
617
+ ```
618
+
619
+ ## 🔧 Basic Usage
620
+
621
+ **Note:** This gem provides base classes for implementers. End users should use
622
+ [commonmarker-merge][commonmarker-merge] or
623
+ [markly-merge][markly-merge] instead.
624
+
625
+ ### For End Users
626
+
627
+ Use a parser-specific implementation:
628
+
629
+ ```ruby
630
+ # Option 1: Using commonmarker-merge (Comrak/Rust)
631
+ require "commonmarker/merge"
632
+
633
+ template = File.read("template.md")
634
+ destination = File.read("destination.md")
635
+
636
+ merger = Commonmarker::Merge::SmartMerger.new(template, destination)
637
+ result = merger.merge
638
+
639
+ File.write("merged.md", result.content)
640
+ ```
641
+
642
+ ```ruby
643
+ # Option 2: Using markly-merge (libcmark-gfm/C)
644
+ require "markly/merge"
645
+
646
+ template = File.read("template.md")
647
+ destination = File.read("destination.md")
648
+
649
+ merger = Markly::Merge::SmartMerger.new(template, destination)
650
+ result = merger.merge
651
+
652
+ File.write("merged.md", result.to_markdown)
653
+ ```
654
+
655
+ ### For Implementers
656
+
657
+ Creating a new parser-specific implementation:
658
+
659
+ ```ruby
660
+ require "markdown/merge"
661
+
662
+ module MyParser
663
+ module Merge
664
+ class FileAnalysis < Markdown::Merge::FileAnalysisBase
665
+ def parse_document(source)
666
+ # Parse source and return root document node
667
+ MyParser.parse(source)
668
+ end
669
+
670
+ def next_sibling(node)
671
+ # Return the next sibling of a node
672
+ node.next_sibling
673
+ end
674
+
675
+ def compute_parser_signature(node)
676
+ # Compute signature for parser-specific nodes
677
+ # Or call super for default implementation
678
+ super
679
+ end
680
+ end
681
+
682
+ class SmartMerger < Markdown::Merge::SmartMergerBase
683
+ def create_file_analysis(content, **options)
684
+ FileAnalysis.new(content, **options)
685
+ end
686
+
687
+ def node_to_source(node, analysis)
688
+ case node
689
+ when Markdown::Merge::FreezeNode
690
+ node.full_text
691
+ else
692
+ # Convert node back to source text
693
+ node.to_markdown
694
+ end
695
+ end
696
+ end
697
+ end
698
+ end
699
+ ```
700
+
701
+ ### Freeze Block Protection
702
+
703
+ Both implementations support freeze blocks for protecting customized sections:
704
+
705
+ ```markdown
706
+ # My Project
707
+
708
+ ## Installation
709
+
710
+ <!-- markdown-merge:freeze Custom install instructions -->
711
+ This installation section has been customized and will be preserved
712
+ during template merges, regardless of what the template contains.
713
+ <!-- markdown-merge:unfreeze -->
714
+
715
+ ## Usage
716
+
717
+ Standard usage section - can be updated from template.
718
+ ```
719
+
720
+ Content between freeze markers is always preserved from the destination file,
721
+ even when the template has different content for that section.
722
+
723
+ ## 🦷 FLOSS Funding
724
+
725
+ While kettle-rb tools are free software and will always be, the project would benefit immensely from some funding.
726
+ Raising a monthly budget of... "dollars" would make the project more sustainable.
727
+
728
+ We welcome both individual and corporate sponsors\! We also offer a
729
+ wide array of funding channels to account for your preferences
730
+ (although currently [Open Collective][🖇osc] is our preferred funding platform).
731
+
732
+ **If you're working in a company that's making significant use of kettle-rb tools we'd
733
+ appreciate it if you suggest to your company to become a kettle-rb sponsor.**
734
+
735
+ You can support the development of kettle-rb tools via
736
+ [GitHub Sponsors][🖇sponsor],
737
+ [Liberapay][⛳liberapay],
738
+ [PayPal][🖇paypal],
739
+ [Open Collective][🖇osc]
740
+ and [Tidelift][🏙️entsup-tidelift].
741
+
742
+ | 📍 NOTE |
743
+ | --- |
744
+ | If doing a sponsorship in the form of donation is problematic for your company <br/> from an accounting standpoint, we'd recommend the use of Tidelift, <br/> where you can get a support-like subscription instead. |
745
+
746
+ ### Open Collective for Individuals
747
+
748
+ Support us with a monthly donation and help us continue our activities. \[[Become a backer][🖇osc-backers]\]
749
+
750
+ NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
751
+
752
+ <!-- OPENCOLLECTIVE-INDIVIDUALS:START -->
753
+ No backers yet. Be the first\!
754
+ <!-- OPENCOLLECTIVE-INDIVIDUALS:END -->
755
+
756
+ ### Open Collective for Organizations
757
+
758
+ Become a sponsor and get your logo on our README on GitHub with a link to your site. \[[Become a sponsor][🖇osc-sponsors]\]
759
+
760
+ NOTE: [kettle-readme-backers][kettle-readme-backers] updates this list every day, automatically.
761
+
762
+ <!-- OPENCOLLECTIVE-ORGANIZATIONS:START -->
763
+ No sponsors yet. Be the first\!
764
+ <!-- OPENCOLLECTIVE-ORGANIZATIONS:END -->
765
+
766
+ [kettle-readme-backers]: https://github.com/kettle-rb/markdown-merge/blob/main/exe/kettle-readme-backers
767
+
768
+ ### Another way to support open-source
769
+
770
+ I’m driven by a passion to foster a thriving open-source community – a space where people can tackle complex problems, no matter how small. Revitalizing libraries that have fallen into disrepair, and building new libraries focused on solving real-world challenges, are my passions. I was recently affected by layoffs, and the tech jobs market is unwelcoming. I’m reaching out here because your support would significantly aid my efforts to provide for my family, and my farm (11 🐔 chickens, 2 🐶 dogs, 3 🐰 rabbits, 8 🐈‍ cats).
771
+
772
+ If you work at a company that uses my work, please encourage them to support me as a corporate sponsor. My work on gems you use might show up in `bundle fund`.
773
+
774
+ I’m developing a new library, [floss\_funding][🖇floss-funding-gem], designed to empower open-source developers like myself to get paid for the work we do, in a sustainable way. Please give it a look.
775
+
776
+ **[Floss-Funding.dev][🖇floss-funding.dev]: 👉️ No network calls. 👉️ No tracking. 👉️ No oversight. 👉️ Minimal crypto hashing. 💡 Easily disabled nags**
777
+
778
+ [![OpenCollective Backers][🖇osc-backers-i]][🖇osc-backers] [![OpenCollective Sponsors][🖇osc-sponsors-i]][🖇osc-sponsors] [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate on Polar][🖇polar-img]][🖇polar] [![Donate to my FLOSS efforts at ko-fi.com][🖇kofi-img]][🖇kofi] [![Donate to my FLOSS efforts using Patreon][🖇patreon-img]][🖇patreon]
779
+
780
+ ## 🔐 Security
781
+
782
+ See [SECURITY.md][🔐security].
783
+
784
+ ## 🤝 Contributing
785
+
786
+ If you need some ideas of where to help, you could work on adding more code coverage,
787
+ or if it is already 💯 (see [below](#code-coverage)) check [reek](REEK), [issues][🤝gh-issues], or [PRs][🤝gh-pulls],
788
+ or use the gem and think about how it could be better.
789
+
790
+ We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
791
+
792
+ See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
793
+
794
+ ### 🚀 Release Instructions
795
+
796
+ See [CONTRIBUTING.md][🤝contributing].
797
+
798
+ ### Code Coverage
799
+
800
+ [![Coverage Graph][🏀codecov-g]][🏀codecov]
801
+
802
+ [![Coveralls Test Coverage][🏀coveralls-img]][🏀coveralls]
803
+
804
+ [![QLTY Test Coverage][🏀qlty-covi]][🏀qlty-cov]
805
+
806
+ ### 🪇 Code of Conduct
807
+
808
+ Everyone interacting with this project's codebases, issue trackers,
809
+ chat rooms and mailing lists agrees to follow the [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct].
810
+
811
+ ## 🌈 Contributors
812
+
813
+ [![Contributors][🖐contributors-img]][🖐contributors]
814
+
815
+ Made with [contributors-img][🖐contrib-rocks].
816
+
817
+ Also see GitLab Contributors: <https://gitlab.com/kettle-rb/markdown-merge/-/graphs/main>
818
+
819
+ <details>
820
+ <summary>⭐️ Star History</summary>
821
+
822
+ <a href="https://star-history.com/#kettle-rb/markdown-merge&Date">
823
+ <picture>
824
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kettle-rb/markdown-merge&type=Date&theme=dark" />
825
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kettle-rb/markdown-merge&type=Date" />
826
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kettle-rb/markdown-merge&type=Date" />
827
+ </picture>
828
+ </a>
829
+
830
+ </details>
831
+
832
+ ## 📌 Versioning
833
+
834
+ This Library adheres to [![Semantic Versioning 2.0.0][📌semver-img]][📌semver].
835
+ Violations of this scheme should be reported as bugs.
836
+ Specifically, if a minor or patch version is released that breaks backward compatibility,
837
+ a new version should be immediately released that restores compatibility.
838
+ Breaking changes to the public API will only be introduced with new major versions.
839
+
840
+ > dropping support for a platform is both obviously and objectively a breaking change <br/>
841
+ > —Jordan Harband ([@ljharb](https://github.com/ljharb), maintainer of SemVer) [in SemVer issue 716][📌semver-breaking]
842
+
843
+ I understand that policy doesn't work universally ("exceptions to every rule\!"),
844
+ but it is the policy here.
845
+ As such, in many cases it is good to specify a dependency on this library using
846
+ the [Pessimistic Version Constraint][📌pvc] with two digits of precision.
847
+
848
+ For example:
849
+
850
+ ```ruby
851
+ spec.add_dependency("markdown-merge", "~> 1.0")
852
+ ```
853
+
854
+ <details markdown="1">
855
+ <summary>📌 Is "Platform Support" part of the public API? More details inside.</summary>
856
+
857
+ SemVer should, IMO, but doesn't explicitly, say that dropping support for specific Platforms
858
+ is a *breaking change* to an API, and for that reason the bike shedding is endless.
859
+
860
+ To get a better understanding of how SemVer is intended to work over a project's lifetime,
861
+ read this article from the creator of SemVer:
862
+
863
+ - ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred]
864
+
865
+ </details>
866
+
867
+ See [CHANGELOG.md][📌changelog] for a list of releases.
868
+
869
+ ## 📄 License
870
+
871
+ The gem is available as open source under the terms of
872
+ the [MIT License][📄license] [![License: MIT][📄license-img]][📄license-ref].
873
+ See [LICENSE.txt][📄license] for the official [Copyright Notice][📄copyright-notice-explainer].
874
+
875
+ ### © Copyright
876
+
877
+ <ul>
878
+ <li>
879
+ Copyright (c) 2025 Peter H. Boling, of
880
+ <a href="https://discord.gg/3qme4XHNKN">
881
+ Galtzo.com
882
+ <picture>
883
+ <img src="https://logos.galtzo.com/assets/images/galtzo-floss/avatar-128px-blank.svg" alt="Galtzo.com Logo (Wordless) by Aboling0, CC BY-SA 4.0" width="24">
884
+ </picture>
885
+ </a>, and markdown-merge contributors.
886
+ </li>
887
+ </ul>
888
+
889
+ ## 🤑 A request for help
890
+
891
+ Maintainers have teeth and need to pay their dentists.
892
+ After getting laid off in an RIF in March, and encountering difficulty finding a new one,
893
+ I began spending most of my time building open source tools.
894
+ I'm hoping to be able to pay for my kids' health insurance this month,
895
+ so if you value the work I am doing, I need your support.
896
+ Please consider sponsoring me or the project.
897
+
898
+ To join the community or get help 👇️ Join the Discord.
899
+
900
+ [![Live Chat on Discord][✉️discord-invite-img-ftb]][🖼️galtzo-discord]
901
+
902
+ To say "thanks\!" ☝️ Join the Discord or 👇️ send money.
903
+
904
+ [![Sponsor kettle-rb/markdown-merge on Open Source Collective][🖇osc-all-bottom-img]][🖇osc] 💌 [![Sponsor me on GitHub Sponsors][🖇sponsor-bottom-img]][🖇sponsor] 💌 [![Sponsor me on Liberapay][⛳liberapay-bottom-img]][⛳liberapay] 💌 [![Donate on PayPal][🖇paypal-bottom-img]][🖇paypal]
905
+
906
+ ### Please give the project a star ⭐ ♥.
907
+
908
+ Thanks for RTFM. ☺️
909
+
910
+ [⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay&color=a51611&style=flat
911
+ [⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/pboling.svg?style=for-the-badge&logo=liberapay&color=a51611
912
+ [⛳liberapay]: https://liberapay.com/pboling/donate
913
+ [🖇osc-all-img]: https://img.shields.io/opencollective/all/kettle-rb
914
+ [🖇osc-sponsors-img]: https://img.shields.io/opencollective/sponsors/kettle-rb
915
+ [🖇osc-backers-img]: https://img.shields.io/opencollective/backers/kettle-rb
916
+ [🖇osc-backers]: https://opencollective.com/kettle-rb#backer
917
+ [🖇osc-backers-i]: https://opencollective.com/kettle-rb/backers/badge.svg?style=flat
918
+ [🖇osc-sponsors]: https://opencollective.com/kettle-rb#sponsor
919
+ [🖇osc-sponsors-i]: https://opencollective.com/kettle-rb/sponsors/badge.svg?style=flat
920
+ [🖇osc-all-bottom-img]: https://img.shields.io/opencollective/all/kettle-rb?style=for-the-badge
921
+ [🖇osc-sponsors-bottom-img]: https://img.shields.io/opencollective/sponsors/kettle-rb?style=for-the-badge
922
+ [🖇osc-backers-bottom-img]: https://img.shields.io/opencollective/backers/kettle-rb?style=for-the-badge
923
+ [🖇osc]: https://opencollective.com/kettle-rb
924
+ [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github
925
+ [🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-pboling-blue?style=for-the-badge&logo=github
926
+ [🖇sponsor]: https://github.com/sponsors/pboling
927
+ [🖇polar-img]: https://img.shields.io/badge/polar-donate-a51611.svg?style=flat
928
+ [🖇polar]: https://polar.sh/pboling
929
+ [🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat
930
+ [🖇kofi]: https://ko-fi.com/O5O86SNP4
931
+ [🖇patreon-img]: https://img.shields.io/badge/patreon-donate-a51611.svg?style=flat
932
+ [🖇patreon]: https://patreon.com/galtzo
933
+ [🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat
934
+ [🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug=pboling&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff
935
+ [🖇buyme]: https://www.buymeacoffee.com/pboling
936
+ [🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal
937
+ [🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A
938
+ [🖇paypal]: https://www.paypal.com/paypalme/peterboling
939
+ [🖇floss-funding.dev]: https://floss-funding.dev
940
+ [🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding
941
+ [✉️discord-invite]: https://discord.gg/3qme4XHNKN
942
+ [✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord
943
+ [✉️ruby-friends-img]: https://img.shields.io/badge/daily.dev-%F0%9F%92%8E_Ruby_Friends-0A0A0A?style=for-the-badge&logo=dailydotdev&logoColor=white
944
+ [✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends
945
+ [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
946
+ [⛳️gem-namespace]: https://github.com/kettle-rb/markdown-merge
947
+ [⛳️namespace-img]: https://img.shields.io/badge/namespace-Markdown::Merge-3C2D2D.svg?style=square&logo=ruby&logoColor=white
948
+ [⛳️gem-name]: https://bestgems.org/gems/markdown-merge
949
+ [⛳️name-img]: https://img.shields.io/badge/name-markdown--merge-3C2D2D.svg?style=square&logo=rubygems&logoColor=red
950
+ [⛳️tag-img]: https://img.shields.io/github/tag/kettle-rb/markdown-merge.svg
951
+ [⛳️tag]: http://github.com/kettle-rb/markdown-merge/releases
952
+ [🚂maint-blog]: http://www.railsbling.com/tags/markdown-merge
953
+ [🚂maint-blog-img]: https://img.shields.io/badge/blog-railsbling-0093D0.svg?style=for-the-badge&logo=rubyonrails&logoColor=orange
954
+ [🚂maint-contact]: http://www.railsbling.com/contact
955
+ [🚂maint-contact-img]: https://img.shields.io/badge/Contact-Maintainer-0093D0.svg?style=flat&logo=rubyonrails&logoColor=red
956
+ [💖🖇linkedin]: http://www.linkedin.com/in/peterboling
957
+ [💖🖇linkedin-img]: https://img.shields.io/badge/PeterBoling-LinkedIn-0B66C2?style=flat&logo=newjapanprowrestling
958
+ [💖✌️wellfound]: https://wellfound.com/u/peter-boling
959
+ [💖✌️wellfound-img]: https://img.shields.io/badge/peter--boling-orange?style=flat&logo=wellfound
960
+ [💖💲crunchbase]: https://www.crunchbase.com/person/peter-boling
961
+ [💖💲crunchbase-img]: https://img.shields.io/badge/peter--boling-purple?style=flat&logo=crunchbase
962
+ [💖🐘ruby-mast]: https://ruby.social/@galtzo
963
+ [💖🐘ruby-mast-img]: https://img.shields.io/mastodon/follow/109447111526622197?domain=https://ruby.social&style=flat&logo=mastodon&label=Ruby%20@galtzo
964
+ [💖🦋bluesky]: https://bsky.app/profile/galtzo.com
965
+ [💖🦋bluesky-img]: https://img.shields.io/badge/@galtzo.com-0285FF?style=flat&logo=bluesky&logoColor=white
966
+ [💖🌳linktree]: https://linktr.ee/galtzo
967
+ [💖🌳linktree-img]: https://img.shields.io/badge/galtzo-purple?style=flat&logo=linktree
968
+ [💖💁🏼‍♂️devto]: https://dev.to/galtzo
969
+ [💖💁🏼‍♂️devto-img]: https://img.shields.io/badge/dev.to-0A0A0A?style=flat&logo=devdotto&logoColor=white
970
+ [💖💁🏼‍♂️aboutme]: https://about.me/peter.boling
971
+ [💖💁🏼‍♂️aboutme-img]: https://img.shields.io/badge/about.me-0A0A0A?style=flat&logo=aboutme&logoColor=white
972
+ [💖🧊berg]: https://codeberg.org/pboling
973
+ [💖🐙hub]: https://github.org/pboling
974
+ [💖🛖hut]: https://sr.ht/~galtzo/
975
+ [💖🧪lab]: https://gitlab.com/pboling
976
+ [👨🏼‍🏫expsup-upwork]: https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share
977
+ [👨🏼‍🏫expsup-upwork-img]: https://img.shields.io/badge/UpWork-13544E?style=for-the-badge&logo=Upwork&logoColor=white
978
+ [👨🏼‍🏫expsup-codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github
979
+ [👨🏼‍🏫expsup-codementor-img]: https://img.shields.io/badge/CodeMentor-Get_Help-1abc9c?style=for-the-badge&logo=CodeMentor&logoColor=white
980
+ [🏙️entsup-tidelift]: https://tidelift.com/subscription/pkg/rubygems-markdown-merge?utm_source=rubygems-markdown-merge&utm_medium=referral&utm_campaign=readme
981
+ [🏙️entsup-tidelift-img]: https://img.shields.io/badge/Tidelift_and_Sonar-Enterprise_Support-FD3456?style=for-the-badge&logo=sonar&logoColor=white
982
+ [🏙️entsup-tidelift-sonar]: https://blog.tidelift.com/tidelift-joins-sonar
983
+ [💁🏼‍♂️peterboling]: http://www.peterboling.com
984
+ [🚂railsbling]: http://www.railsbling.com
985
+ [📜src-gl-img]: https://img.shields.io/badge/GitLab-FBA326?style=for-the-badge&logo=Gitlab&logoColor=orange
986
+ [📜src-gl]: https://gitlab.com/kettle-rb/markdown-merge/
987
+ [📜src-cb-img]: https://img.shields.io/badge/CodeBerg-4893CC?style=for-the-badge&logo=CodeBerg&logoColor=blue
988
+ [📜src-cb]: https://codeberg.org/kettle-rb/markdown-merge
989
+ [📜src-gh-img]: https://img.shields.io/badge/GitHub-238636?style=for-the-badge&logo=Github&logoColor=green
990
+ [📜src-gh]: https://github.com/kettle-rb/markdown-merge
991
+ [📜docs-cr-rd-img]: https://img.shields.io/badge/RubyDoc-Current_Release-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
992
+ [📜docs-head-rd-img]: https://img.shields.io/badge/YARD_on_Galtzo.com-HEAD-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
993
+ [📜gl-wiki]: https://gitlab.com/kettle-rb/markdown-merge/-/wikis/home
994
+ [📜gh-wiki]: https://github.com/kettle-rb/markdown-merge/wiki
995
+ [📜gl-wiki-img]: https://img.shields.io/badge/wiki-examples-943CD2.svg?style=for-the-badge&logo=gitlab&logoColor=white
996
+ [📜gh-wiki-img]: https://img.shields.io/badge/wiki-examples-943CD2.svg?style=for-the-badge&logo=github&logoColor=white
997
+ [👽dl-rank]: https://bestgems.org/gems/markdown-merge
998
+ [👽dl-ranki]: https://img.shields.io/gem/rd/markdown-merge.svg
999
+ [👽oss-help]: https://www.codetriage.com/kettle-rb/markdown-merge
1000
+ [👽oss-helpi]: https://www.codetriage.com/kettle-rb/markdown-merge/badges/users.svg
1001
+ [👽version]: https://bestgems.org/gems/markdown-merge
1002
+ [👽versioni]: https://img.shields.io/gem/v/markdown-merge.svg
1003
+ [🏀qlty-mnt]: https://qlty.sh/gh/kettle-rb/projects/markdown-merge
1004
+ [🏀qlty-mnti]: https://qlty.sh/gh/kettle-rb/projects/markdown-merge/maintainability.svg
1005
+ [🏀qlty-cov]: https://qlty.sh/gh/kettle-rb/projects/markdown-merge/metrics/code?sort=coverageRating
1006
+ [🏀qlty-covi]: https://qlty.sh/gh/kettle-rb/projects/markdown-merge/coverage.svg
1007
+ [🏀codecov]: https://codecov.io/gh/kettle-rb/markdown-merge
1008
+ [🏀codecovi]: https://codecov.io/gh/kettle-rb/markdown-merge/graph/badge.svg
1009
+ [🏀coveralls]: https://coveralls.io/github/kettle-rb/markdown-merge?branch=main
1010
+ [🏀coveralls-img]: https://coveralls.io/repos/github/kettle-rb/markdown-merge/badge.svg?branch=main
1011
+ [🖐codeQL]: https://github.com/kettle-rb/markdown-merge/security/code-scanning
1012
+ [🖐codeQL-img]: https://github.com/kettle-rb/markdown-merge/actions/workflows/codeql-analysis.yml/badge.svg
1013
+ [🚎2-cov-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/coverage.yml
1014
+ [🚎2-cov-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/coverage.yml/badge.svg
1015
+ [🚎3-hd-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/heads.yml
1016
+ [🚎3-hd-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/heads.yml/badge.svg
1017
+ [🚎5-st-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/style.yml
1018
+ [🚎5-st-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/style.yml/badge.svg
1019
+ [🚎6-s-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/supported.yml
1020
+ [🚎6-s-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/supported.yml/badge.svg
1021
+ [🚎9-t-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/truffle.yml
1022
+ [🚎9-t-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/truffle.yml/badge.svg
1023
+ [🚎11-c-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml
1024
+ [🚎11-c-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml/badge.svg
1025
+ [🚎12-crh-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/dep-heads.yml
1026
+ [🚎12-crh-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/dep-heads.yml/badge.svg
1027
+ [🚎13-🔒️-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/locked_deps.yml
1028
+ [🚎13-🔒️-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/locked_deps.yml/badge.svg
1029
+ [🚎14-🔓️-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/unlocked_deps.yml
1030
+ [🚎14-🔓️-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/unlocked_deps.yml/badge.svg
1031
+ [🚎15-🪪-wf]: https://github.com/kettle-rb/markdown-merge/actions/workflows/license-eye.yml
1032
+ [🚎15-🪪-wfi]: https://github.com/kettle-rb/markdown-merge/actions/workflows/license-eye.yml/badge.svg
1033
+ [💎ruby-3.2i]: https://img.shields.io/badge/Ruby-3.2-CC342D?style=for-the-badge&logo=ruby&logoColor=white
1034
+ [💎ruby-3.3i]: https://img.shields.io/badge/Ruby-3.3-CC342D?style=for-the-badge&logo=ruby&logoColor=white
1035
+ [💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green
1036
+ [💎ruby-headi]: https://img.shields.io/badge/Ruby-HEAD-CC342D?style=for-the-badge&logo=ruby&logoColor=blue
1037
+ [💎truby-23.1i]: https://img.shields.io/badge/Truffle_Ruby-23.1-34BCB1?style=for-the-badge&logo=ruby&logoColor=pink
1038
+ [💎truby-c-i]: https://img.shields.io/badge/Truffle_Ruby-current-34BCB1?style=for-the-badge&logo=ruby&logoColor=green
1039
+ [💎truby-headi]: https://img.shields.io/badge/Truffle_Ruby-HEAD-34BCB1?style=for-the-badge&logo=ruby&logoColor=blue
1040
+ [💎jruby-c-i]: https://img.shields.io/badge/JRuby-current-FBE742?style=for-the-badge&logo=ruby&logoColor=green
1041
+ [💎jruby-headi]: https://img.shields.io/badge/JRuby-HEAD-FBE742?style=for-the-badge&logo=ruby&logoColor=blue
1042
+ [🤝gh-issues]: https://github.com/kettle-rb/markdown-merge/issues
1043
+ [🤝gh-pulls]: https://github.com/kettle-rb/markdown-merge/pulls
1044
+ [🤝gl-issues]: https://gitlab.com/kettle-rb/markdown-merge/-/issues
1045
+ [🤝gl-pulls]: https://gitlab.com/kettle-rb/markdown-merge/-/merge_requests
1046
+ [🤝cb-issues]: https://codeberg.org/kettle-rb/markdown-merge/issues
1047
+ [🤝cb-pulls]: https://codeberg.org/kettle-rb/markdown-merge/pulls
1048
+ [🤝cb-donate]: https://donate.codeberg.org/
1049
+ [🤝contributing]: CONTRIBUTING.md
1050
+ [🏀codecov-g]: https://codecov.io/gh/kettle-rb/markdown-merge/graphs/tree.svg
1051
+ [🖐contrib-rocks]: https://contrib.rocks
1052
+ [🖐contributors]: https://github.com/kettle-rb/markdown-merge/graphs/contributors
1053
+ [🖐contributors-img]: https://contrib.rocks/image?repo=kettle-rb/markdown-merge
1054
+ [🚎contributors-gl]: https://gitlab.com/kettle-rb/markdown-merge/-/graphs/main
1055
+ [🪇conduct]: CODE_OF_CONDUCT.md
1056
+ [🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-259D6C.svg
1057
+ [📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
1058
+ [📌semver]: https://semver.org/spec/v2.0.0.html
1059
+ [📌semver-img]: https://img.shields.io/badge/semver-2.0.0-259D6C.svg?style=flat
1060
+ [📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139
1061
+ [📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html
1062
+ [📌changelog]: CHANGELOG.md
1063
+ [📗keep-changelog]: https://keepachangelog.com/en/1.0.0/
1064
+ [📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-34495e.svg?style=flat
1065
+ [📌gitmoji]: https://gitmoji.dev
1066
+ [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1067
+ [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1068
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.972-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1069
+ [🔐security]: SECURITY.md
1070
+ [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1071
+ [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
1072
+ [📄license]: LICENSE.txt
1073
+ [📄license-ref]: https://opensource.org/licenses/MIT
1074
+ [📄license-img]: https://img.shields.io/badge/License-MIT-259D6C.svg
1075
+ [📄license-compat]: https://dev.to/galtzo/how-to-check-license-compatibility-41h0
1076
+ [📄license-compat-img]: https://img.shields.io/badge/Apache_Compatible:_Category_A-%E2%9C%93-259D6C.svg?style=flat&logo=Apache
1077
+ [📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm
1078
+ [📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat
1079
+ [🚎yard-current]: http://rubydoc.info/gems/markdown-merge
1080
+ [🚎yard-head]: https://markdown-merge.galtzo.com
1081
+ [💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums
1082
+ [💎SHA_checksums]: https://gitlab.com/kettle-rb/markdown-merge/-/tree/main/checksums
1083
+ [💎rlts]: https://github.com/rubocop-lts/rubocop-lts
1084
+ [💎rlts-img]: https://img.shields.io/badge/code_style_&_linting-rubocop--lts-34495e.svg?plastic&logo=ruby&logoColor=white
1085
+ [💎appraisal2]: https://github.com/appraisal-rb/appraisal2
1086
+ [💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
1087
+ [💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/