markly-merge 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +87 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +227 -0
- data/FUNDING.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +815 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/markly/merge/backend.rb +422 -0
- data/lib/markly/merge/debug_logger.rb +26 -0
- data/lib/markly/merge/file_analysis.rb +53 -0
- data/lib/markly/merge/freeze_node.rb +32 -0
- data/lib/markly/merge/smart_merger.rb +159 -0
- data/lib/markly/merge/version.rb +12 -0
- data/lib/markly/merge.rb +110 -0
- data/lib/markly-merge.rb +6 -0
- data/sig/markly/merge.rbs +106 -0
- data.tar.gz.sig +0 -0
- metadata +368 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markly
|
|
4
|
+
module Merge
|
|
5
|
+
# Orchestrates the smart merge process for Markdown files using Markly.
|
|
6
|
+
#
|
|
7
|
+
# This is a thin wrapper around Markdown::Merge::SmartMerger that:
|
|
8
|
+
# - Forces the :markly backend
|
|
9
|
+
# - Sets markly-specific defaults (freeze token, inner_merge_code_blocks)
|
|
10
|
+
# - Exposes markly-specific options (flags, extensions)
|
|
11
|
+
#
|
|
12
|
+
# @example Basic merge (destination customizations preserved)
|
|
13
|
+
# merger = SmartMerger.new(template_content, dest_content)
|
|
14
|
+
# result = merger.merge
|
|
15
|
+
# if result.success?
|
|
16
|
+
# File.write("output.md", result.content)
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Template updates win
|
|
20
|
+
# merger = SmartMerger.new(
|
|
21
|
+
# template_content,
|
|
22
|
+
# dest_content,
|
|
23
|
+
# preference: :template,
|
|
24
|
+
# add_template_only_nodes: true
|
|
25
|
+
# )
|
|
26
|
+
# result = merger.merge
|
|
27
|
+
#
|
|
28
|
+
# @example Custom signature matching
|
|
29
|
+
# sig_gen = ->(node) {
|
|
30
|
+
# canonical_type = Ast::Merge::NodeTyping.merge_type_for(node) || node.type
|
|
31
|
+
# if canonical_type == :heading
|
|
32
|
+
# [:heading, node.header_level] # Match by level only, not content
|
|
33
|
+
# else
|
|
34
|
+
# node # Fall through to default
|
|
35
|
+
# end
|
|
36
|
+
# }
|
|
37
|
+
# merger = SmartMerger.new(
|
|
38
|
+
# template_content,
|
|
39
|
+
# dest_content,
|
|
40
|
+
# signature_generator: sig_gen
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
43
|
+
# @example Disable inner-merge for code blocks
|
|
44
|
+
# merger = SmartMerger.new(
|
|
45
|
+
# template_content,
|
|
46
|
+
# dest_content,
|
|
47
|
+
# inner_merge_code_blocks: false
|
|
48
|
+
# )
|
|
49
|
+
#
|
|
50
|
+
# @see Markdown::Merge::SmartMerger Underlying implementation
|
|
51
|
+
class SmartMerger < Markdown::Merge::SmartMerger
|
|
52
|
+
# Creates a new SmartMerger for intelligent Markdown file merging.
|
|
53
|
+
#
|
|
54
|
+
# @param template_content [String] Template Markdown source code
|
|
55
|
+
# @param dest_content [String] Destination Markdown source code
|
|
56
|
+
#
|
|
57
|
+
# @param signature_generator [Proc, nil] Optional proc to generate custom node signatures.
|
|
58
|
+
# The proc receives a node (wrapped with canonical merge_type) and should return one of:
|
|
59
|
+
# - An array representing the node's signature
|
|
60
|
+
# - `nil` to indicate the node should have no signature
|
|
61
|
+
# - The original node to fall through to default signature computation
|
|
62
|
+
#
|
|
63
|
+
# @param preference [Symbol] Controls which version to use when nodes
|
|
64
|
+
# have matching signatures but different content:
|
|
65
|
+
# - `:destination` (default) - Use destination version (preserves customizations)
|
|
66
|
+
# - `:template` - Use template version (applies updates)
|
|
67
|
+
#
|
|
68
|
+
# @param add_template_only_nodes [Boolean] Controls whether to add nodes that only
|
|
69
|
+
# exist in template:
|
|
70
|
+
# - `false` (default) - Skip template-only nodes
|
|
71
|
+
# - `true` - Add template-only nodes to result
|
|
72
|
+
#
|
|
73
|
+
# @param inner_merge_code_blocks [Boolean, CodeBlockMerger] Controls inner-merge for
|
|
74
|
+
# fenced code blocks:
|
|
75
|
+
# - `true` (default for markly-merge) - Enable inner-merge using default CodeBlockMerger
|
|
76
|
+
# - `false` - Disable inner-merge (use standard conflict resolution)
|
|
77
|
+
# - `CodeBlockMerger` instance - Use custom CodeBlockMerger
|
|
78
|
+
#
|
|
79
|
+
# @param freeze_token [String] Token to use for freeze block markers.
|
|
80
|
+
# Default: "markly-merge"
|
|
81
|
+
# Looks for: <!-- markly-merge:freeze --> / <!-- markly-merge:unfreeze -->
|
|
82
|
+
#
|
|
83
|
+
# @param flags [Integer] Markly parse flags (e.g., Markly::FOOTNOTES | Markly::SMART).
|
|
84
|
+
# Default: Markly::DEFAULT
|
|
85
|
+
#
|
|
86
|
+
# @param extensions [Array<Symbol>] Markly extensions to enable (e.g., [:table, :strikethrough])
|
|
87
|
+
# Available extensions: :table, :strikethrough, :autolink, :tagfilter, :tasklist
|
|
88
|
+
#
|
|
89
|
+
# @param match_refiner [#call, nil] Optional match refiner for fuzzy matching of
|
|
90
|
+
# unmatched nodes. Default: nil (fuzzy matching disabled).
|
|
91
|
+
#
|
|
92
|
+
# @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
93
|
+
# for per-node-type merge preferences.
|
|
94
|
+
# @param options [Hash] Additional options for forward compatibility
|
|
95
|
+
#
|
|
96
|
+
# @raise [TemplateParseError] If template has syntax errors
|
|
97
|
+
# @raise [DestinationParseError] If destination has syntax errors
|
|
98
|
+
def initialize(
|
|
99
|
+
template_content,
|
|
100
|
+
dest_content,
|
|
101
|
+
signature_generator: nil,
|
|
102
|
+
preference: :destination,
|
|
103
|
+
add_template_only_nodes: false,
|
|
104
|
+
inner_merge_code_blocks: DEFAULT_INNER_MERGE_CODE_BLOCKS,
|
|
105
|
+
freeze_token: DEFAULT_FREEZE_TOKEN,
|
|
106
|
+
flags: ::Markly::DEFAULT,
|
|
107
|
+
extensions: [:table],
|
|
108
|
+
match_refiner: nil,
|
|
109
|
+
node_typing: nil,
|
|
110
|
+
**options
|
|
111
|
+
)
|
|
112
|
+
super(
|
|
113
|
+
template_content,
|
|
114
|
+
dest_content,
|
|
115
|
+
backend: :markly,
|
|
116
|
+
signature_generator: signature_generator,
|
|
117
|
+
preference: preference,
|
|
118
|
+
add_template_only_nodes: add_template_only_nodes,
|
|
119
|
+
inner_merge_code_blocks: inner_merge_code_blocks,
|
|
120
|
+
freeze_token: freeze_token,
|
|
121
|
+
match_refiner: match_refiner,
|
|
122
|
+
node_typing: node_typing,
|
|
123
|
+
flags: flags,
|
|
124
|
+
extensions: extensions,
|
|
125
|
+
**options
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns the TemplateParseError class to use.
|
|
130
|
+
#
|
|
131
|
+
# @return [Class] Markly::Merge::TemplateParseError
|
|
132
|
+
def template_parse_error_class
|
|
133
|
+
TemplateParseError
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns the DestinationParseError class to use.
|
|
137
|
+
#
|
|
138
|
+
# @return [Class] Markly::Merge::DestinationParseError
|
|
139
|
+
def destination_parse_error_class
|
|
140
|
+
DestinationParseError
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Create a FileAnalysis instance for parsing.
|
|
144
|
+
#
|
|
145
|
+
# @param content [String] Markdown content to analyze
|
|
146
|
+
# @param options [Hash] Analysis options
|
|
147
|
+
# @return [Markly::Merge::FileAnalysis] File analysis instance
|
|
148
|
+
def create_file_analysis(content, **opts)
|
|
149
|
+
FileAnalysis.new(
|
|
150
|
+
content,
|
|
151
|
+
freeze_token: opts[:freeze_token],
|
|
152
|
+
signature_generator: opts[:signature_generator],
|
|
153
|
+
flags: opts[:flags] || ::Markly::DEFAULT,
|
|
154
|
+
extensions: opts[:extensions] || [:table],
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markly
|
|
4
|
+
module Merge
|
|
5
|
+
# Version information for Markly::Merge
|
|
6
|
+
module Version
|
|
7
|
+
# Current version of the markly-merge gem
|
|
8
|
+
VERSION = "1.0.0"
|
|
9
|
+
end
|
|
10
|
+
VERSION = Version::VERSION # traditional location
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/markly/merge.rb
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hard dependency - ensures markly gem is installed
|
|
4
|
+
require "markly"
|
|
5
|
+
|
|
6
|
+
# External gems
|
|
7
|
+
require "version_gem"
|
|
8
|
+
|
|
9
|
+
# Shared merge infrastructure (includes tree_haver)
|
|
10
|
+
require "markdown/merge"
|
|
11
|
+
|
|
12
|
+
# This gem
|
|
13
|
+
require_relative "merge/version"
|
|
14
|
+
|
|
15
|
+
module Markly
|
|
16
|
+
# Smart merging for Markdown files using Markly AST.
|
|
17
|
+
#
|
|
18
|
+
# Markly::Merge provides intelligent merging of Markdown files by:
|
|
19
|
+
# - Parsing Markdown into AST using Markly (cmark-gfm) via tree_haver
|
|
20
|
+
# - Matching structural elements (headings, paragraphs, lists, etc.) between files
|
|
21
|
+
# - Preserving frozen sections marked with HTML comments
|
|
22
|
+
# - Resolving conflicts based on configurable preferences
|
|
23
|
+
#
|
|
24
|
+
# This is a thin wrapper around Markdown::Merge that:
|
|
25
|
+
# - Provides hard dependency on the markly gem
|
|
26
|
+
# - Sets markly-specific defaults (freeze token, inner_merge_code_blocks)
|
|
27
|
+
# - Exposes markly-specific options (flags, extensions)
|
|
28
|
+
# - Maintains API compatibility for existing users
|
|
29
|
+
#
|
|
30
|
+
# @example Basic merge
|
|
31
|
+
# merger = Markly::Merge::SmartMerger.new(template, destination)
|
|
32
|
+
# result = merger.merge
|
|
33
|
+
# puts result.content if result.success?
|
|
34
|
+
#
|
|
35
|
+
# @example With freeze blocks
|
|
36
|
+
# # In your Markdown file:
|
|
37
|
+
# # <!-- markly-merge:freeze -->
|
|
38
|
+
# # ## Custom Section
|
|
39
|
+
# # This content is preserved during merges.
|
|
40
|
+
# # <!-- markly-merge:unfreeze -->
|
|
41
|
+
#
|
|
42
|
+
# @see SmartMerger Main entry point for merging
|
|
43
|
+
# @see Markdown::Merge::SmartMerger Underlying implementation
|
|
44
|
+
module Merge
|
|
45
|
+
# Base error class for Markly::Merge
|
|
46
|
+
# Inherits from Markdown::Merge::Error for consistency across merge gems.
|
|
47
|
+
class Error < Markdown::Merge::Error; end
|
|
48
|
+
|
|
49
|
+
# Raised when a Markdown file has parsing errors.
|
|
50
|
+
# Inherits from Markdown::Merge::ParseError for consistency across merge gems.
|
|
51
|
+
class ParseError < Markdown::Merge::ParseError; end
|
|
52
|
+
|
|
53
|
+
# Raised when the template file has syntax errors.
|
|
54
|
+
class TemplateParseError < ParseError; end
|
|
55
|
+
|
|
56
|
+
# Raised when the destination file has syntax errors.
|
|
57
|
+
class DestinationParseError < ParseError; end
|
|
58
|
+
|
|
59
|
+
# Default freeze token for markly-merge
|
|
60
|
+
# @return [String]
|
|
61
|
+
DEFAULT_FREEZE_TOKEN = "markly-merge"
|
|
62
|
+
|
|
63
|
+
# Default inner_merge_code_blocks setting for markly-merge
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
DEFAULT_INNER_MERGE_CODE_BLOCKS = true
|
|
66
|
+
|
|
67
|
+
# Re-export shared classes from markdown-merge
|
|
68
|
+
# Re-export shared classes from markdown-merge
|
|
69
|
+
FileAligner = Markdown::Merge::FileAligner
|
|
70
|
+
ConflictResolver = Markdown::Merge::ConflictResolver
|
|
71
|
+
MergeResult = Markdown::Merge::MergeResult
|
|
72
|
+
TableMatchAlgorithm = Markdown::Merge::TableMatchAlgorithm
|
|
73
|
+
TableMatchRefiner = Markdown::Merge::TableMatchRefiner
|
|
74
|
+
CodeBlockMerger = Markdown::Merge::CodeBlockMerger
|
|
75
|
+
NodeTypeNormalizer = Markdown::Merge::NodeTypeNormalizer
|
|
76
|
+
|
|
77
|
+
autoload :DebugLogger, "markly/merge/debug_logger"
|
|
78
|
+
autoload :FreezeNode, "markly/merge/freeze_node"
|
|
79
|
+
autoload :FileAnalysis, "markly/merge/file_analysis"
|
|
80
|
+
autoload :SmartMerger, "markly/merge/smart_merger"
|
|
81
|
+
autoload :Backend, "markly/merge/backend"
|
|
82
|
+
|
|
83
|
+
# Eagerly load and register backend when this module is loaded
|
|
84
|
+
# This ensures the backend is available for tree_haver before any parsing happens
|
|
85
|
+
class << self
|
|
86
|
+
def ensure_backend_loaded!
|
|
87
|
+
Backend # Access constant to trigger autoload
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Ensure backend is loaded and registered
|
|
94
|
+
Markly::Merge.ensure_backend_loaded!
|
|
95
|
+
|
|
96
|
+
# Register with ast-merge's MergeGemRegistry for RSpec dependency tags
|
|
97
|
+
# Only register if MergeGemRegistry is loaded (i.e., in test environment)
|
|
98
|
+
if defined?(Ast::Merge::RSpec::MergeGemRegistry)
|
|
99
|
+
Ast::Merge::RSpec::MergeGemRegistry.register(
|
|
100
|
+
:markly_merge,
|
|
101
|
+
require_path: "markly/merge",
|
|
102
|
+
merger_class: "Markly::Merge::SmartMerger",
|
|
103
|
+
test_source: "# Test\n\nParagraph",
|
|
104
|
+
category: :markdown,
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
Markly::Merge::Version.class_eval do
|
|
109
|
+
extend VersionGem::Basic
|
|
110
|
+
end
|
data/lib/markly-merge.rb
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module Markly
|
|
2
|
+
module Merge
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
# Base error class for Markly::Merge
|
|
6
|
+
class Error < Markdown::Merge::Error
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Raised when a Markdown file has parsing errors
|
|
10
|
+
class ParseError < Markdown::Merge::ParseError
|
|
11
|
+
def initialize: (?String? message, ?content: String?, ?errors: Array[untyped]) -> void
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Raised when the template file has syntax errors
|
|
15
|
+
class TemplateParseError < ParseError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Raised when the destination file has syntax errors
|
|
19
|
+
class DestinationParseError < ParseError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Re-exported shared classes from markdown-merge
|
|
23
|
+
FileAligner: singleton(Markdown::Merge::FileAligner)
|
|
24
|
+
ConflictResolver: singleton(Markdown::Merge::ConflictResolver)
|
|
25
|
+
MergeResult: singleton(Markdown::Merge::MergeResult)
|
|
26
|
+
TableMatchAlgorithm: singleton(Markdown::Merge::TableMatchAlgorithm)
|
|
27
|
+
TableMatchRefiner: singleton(Markdown::Merge::TableMatchRefiner)
|
|
28
|
+
CodeBlockMerger: singleton(Markdown::Merge::CodeBlockMerger)
|
|
29
|
+
|
|
30
|
+
# Debug logging utility for Markly::Merge
|
|
31
|
+
module DebugLogger
|
|
32
|
+
extend Ast::Merge::DebugLogger
|
|
33
|
+
|
|
34
|
+
def self.env_var_name: () -> String
|
|
35
|
+
def self.env_var_name=: (String name) -> String
|
|
36
|
+
def self.log_prefix: () -> String
|
|
37
|
+
def self.log_prefix=: (String prefix) -> String
|
|
38
|
+
def self.enabled?: () -> bool
|
|
39
|
+
def self.debug: (*untyped args) -> void
|
|
40
|
+
def self.info: (*untyped args) -> void
|
|
41
|
+
def self.warning: (*untyped args) -> void
|
|
42
|
+
def self.time: [T] (String label) { () -> T } -> T
|
|
43
|
+
def self.log_node: (untyped node, ?label: String) -> void
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Represents a frozen block of Markdown content
|
|
47
|
+
class FreezeNode < Ast::Merge::FreezeNodeBase
|
|
48
|
+
def initialize: (
|
|
49
|
+
start_line: Integer,
|
|
50
|
+
end_line: Integer,
|
|
51
|
+
content: String,
|
|
52
|
+
start_marker: String,
|
|
53
|
+
end_marker: String,
|
|
54
|
+
?nodes: Array[untyped],
|
|
55
|
+
?reason: String?
|
|
56
|
+
) -> void
|
|
57
|
+
|
|
58
|
+
def signature: () -> Array[Symbol | String]
|
|
59
|
+
def full_text: () -> String
|
|
60
|
+
def line_count: () -> Integer
|
|
61
|
+
def contains_type?: (Symbol type_symbol) -> bool
|
|
62
|
+
def inspect: () -> String
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# File analysis for Markdown files using Markly
|
|
66
|
+
class FileAnalysis < Markdown::Merge::FileAnalysisBase
|
|
67
|
+
DEFAULT_FREEZE_TOKEN: String
|
|
68
|
+
|
|
69
|
+
def initialize: (
|
|
70
|
+
String source,
|
|
71
|
+
?freeze_token: String,
|
|
72
|
+
?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
|
|
73
|
+
?flags: Integer,
|
|
74
|
+
?extensions: Array[Symbol]
|
|
75
|
+
) -> void
|
|
76
|
+
|
|
77
|
+
def parse_document: (String source) -> untyped
|
|
78
|
+
def next_sibling: (untyped node) -> untyped?
|
|
79
|
+
def freeze_node_class: () -> Class
|
|
80
|
+
def parser_node?: (untyped value) -> bool
|
|
81
|
+
def fallthrough_node?: (untyped value) -> bool
|
|
82
|
+
def compute_parser_signature: (untyped node) -> Array[untyped]?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Orchestrates the smart merge process for Markdown files using Markly
|
|
86
|
+
class SmartMerger < Markdown::Merge::SmartMergerBase
|
|
87
|
+
def initialize: (
|
|
88
|
+
String template_content,
|
|
89
|
+
String dest_content,
|
|
90
|
+
?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
|
|
91
|
+
?preference: Symbol,
|
|
92
|
+
?add_template_only_nodes: bool,
|
|
93
|
+
?inner_merge_code_blocks: (bool | Markdown::Merge::CodeBlockMerger),
|
|
94
|
+
?freeze_token: String,
|
|
95
|
+
?match_refiner: (^(Array[untyped], Array[untyped], Hash[Symbol, untyped]) -> Array[Markdown::Merge::MatchResult])?,
|
|
96
|
+
?flags: Integer,
|
|
97
|
+
?extensions: Array[Symbol]
|
|
98
|
+
) -> void
|
|
99
|
+
|
|
100
|
+
def create_file_analysis: (String content, **untyped options) -> FileAnalysis
|
|
101
|
+
def node_to_source: (untyped node, FileAnalysis analysis) -> String
|
|
102
|
+
def template_parse_error_class: () -> Class
|
|
103
|
+
def destination_parse_error_class: () -> Class
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data.tar.gz.sig
ADDED
|
Binary file
|