markdown-merge 1.0.2 → 7.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 +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/markdown/merge/version.rb +3 -4
  4. data/lib/markdown/merge.rb +538 -137
  5. data/lib/markdown-merge.rb +3 -4
  6. data.tar.gz.sig +0 -0
  7. metadata +28 -283
  8. metadata.gz.sig +0 -0
  9. data/CHANGELOG.md +0 -283
  10. data/CITATION.cff +0 -20
  11. data/CODE_OF_CONDUCT.md +0 -134
  12. data/CONTRIBUTING.md +0 -227
  13. data/FUNDING.md +0 -74
  14. data/LICENSE.txt +0 -21
  15. data/README.md +0 -1090
  16. data/REEK +0 -0
  17. data/RUBOCOP.md +0 -71
  18. data/SECURITY.md +0 -21
  19. data/lib/markdown/merge/cleanse/block_spacing.rb +0 -253
  20. data/lib/markdown/merge/cleanse/code_fence_spacing.rb +0 -294
  21. data/lib/markdown/merge/cleanse/condensed_link_refs.rb +0 -405
  22. data/lib/markdown/merge/cleanse.rb +0 -42
  23. data/lib/markdown/merge/code_block_merger.rb +0 -300
  24. data/lib/markdown/merge/conflict_resolver.rb +0 -128
  25. data/lib/markdown/merge/debug_logger.rb +0 -26
  26. data/lib/markdown/merge/document_problems.rb +0 -190
  27. data/lib/markdown/merge/file_aligner.rb +0 -196
  28. data/lib/markdown/merge/file_analysis.rb +0 -353
  29. data/lib/markdown/merge/file_analysis_base.rb +0 -629
  30. data/lib/markdown/merge/freeze_node.rb +0 -93
  31. data/lib/markdown/merge/gap_line_node.rb +0 -136
  32. data/lib/markdown/merge/link_definition_formatter.rb +0 -49
  33. data/lib/markdown/merge/link_definition_node.rb +0 -157
  34. data/lib/markdown/merge/link_parser.rb +0 -421
  35. data/lib/markdown/merge/link_reference_rehydrator.rb +0 -320
  36. data/lib/markdown/merge/markdown_structure.rb +0 -123
  37. data/lib/markdown/merge/merge_result.rb +0 -166
  38. data/lib/markdown/merge/node_type_normalizer.rb +0 -126
  39. data/lib/markdown/merge/output_builder.rb +0 -166
  40. data/lib/markdown/merge/partial_template_merger.rb +0 -334
  41. data/lib/markdown/merge/smart_merger.rb +0 -221
  42. data/lib/markdown/merge/smart_merger_base.rb +0 -621
  43. data/lib/markdown/merge/table_match_algorithm.rb +0 -504
  44. data/lib/markdown/merge/table_match_refiner.rb +0 -136
  45. data/lib/markdown/merge/whitespace_normalizer.rb +0 -251
  46. data/sig/markdown/merge.rbs +0 -341
@@ -1,251 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Markdown
4
- module Merge
5
- # Normalizes whitespace in markdown documents.
6
- #
7
- # Supports multiple normalization modes:
8
- # - `:basic` (or `true`) - Collapse excessive blank lines (3+ → 2)
9
- # - `:link_refs` - Also remove blank lines between consecutive link reference definitions
10
- # - `:strict` - All of the above normalizations
11
- #
12
- # Uses {LinkParser} for detecting link reference definitions, which supports:
13
- # - Standard definitions: `[label]: url`
14
- # - Definitions with titles: `[label]: url "title"`
15
- # - Angle-bracketed URLs: `[label]: <url>`
16
- # - Emoji in labels: `[🎨logo]: url`
17
- #
18
- # @example Basic normalization (default)
19
- # content = "Hello\n\n\n\nWorld"
20
- # normalized = WhitespaceNormalizer.normalize(content)
21
- # # => "Hello\n\nWorld"
22
- #
23
- # @example With link_refs mode
24
- # content = "[link1]: url1\n\n[link2]: url2"
25
- # normalized = WhitespaceNormalizer.normalize(content, mode: :link_refs)
26
- # # => "[link1]: url1\n[link2]: url2"
27
- #
28
- # @example With problem tracking
29
- # normalizer = WhitespaceNormalizer.new(content, mode: :link_refs)
30
- # result = normalizer.normalize
31
- # normalizer.problems.by_category(:link_ref_spacing)
32
- #
33
- class WhitespaceNormalizer
34
- # Valid normalization modes
35
- MODES = %i[basic link_refs strict].freeze
36
-
37
- # @return [String] The original content
38
- attr_reader :content
39
-
40
- # @return [Symbol] The normalization mode
41
- attr_reader :mode
42
-
43
- # @return [DocumentProblems] Problems found during normalization
44
- attr_reader :problems
45
-
46
- class << self
47
- # Normalize whitespace in content (class method for convenience).
48
- #
49
- # @param content [String] Content to normalize
50
- # @param mode [Symbol, Boolean] Normalization mode (:basic, :link_refs, :strict, or true for :basic)
51
- # @return [String] Normalized content
52
- def normalize(content, mode: :basic)
53
- new(content, mode: mode).normalize
54
- end
55
- end
56
-
57
- # Initialize a new normalizer.
58
- #
59
- # @param content [String] Content to normalize
60
- # @param mode [Symbol, Boolean] Normalization mode (:basic, :link_refs, :strict, or true for :basic)
61
- def initialize(content, mode: :basic)
62
- @content = content
63
- @mode = normalize_mode(mode)
64
- @problems = DocumentProblems.new
65
- @link_parser = LinkParser.new
66
- end
67
-
68
- # Normalize whitespace based on the configured mode.
69
- #
70
- # @return [String] Normalized content
71
- def normalize
72
- result = content.dup
73
-
74
- # Always collapse excessive blank lines (3+ → 2)
75
- result = collapse_excessive_blank_lines(result)
76
-
77
- # Remove blank lines between link refs if mode requires it
78
- if @mode == :link_refs || @mode == :strict
79
- result = remove_blank_lines_between_link_refs(result)
80
- end
81
-
82
- result
83
- end
84
-
85
- # Check if normalization made any changes.
86
- #
87
- # @return [Boolean] true if content had whitespace issues
88
- def changed?
89
- !@problems.empty?
90
- end
91
-
92
- # Get count of normalizations performed.
93
- #
94
- # @return [Integer] Number of whitespace issues fixed
95
- def normalization_count
96
- @problems.count
97
- end
98
-
99
- private
100
-
101
- # Normalize mode parameter to a symbol.
102
- #
103
- # @param mode [Symbol, Boolean] Input mode
104
- # @return [Symbol] Normalized mode
105
- def normalize_mode(mode)
106
- case mode
107
- when true
108
- :basic
109
- when false
110
- :basic # Still do basic normalization
111
- when Symbol
112
- raise ArgumentError, "Unknown mode: #{mode}. Valid modes: #{MODES.join(", ")}" unless MODES.include?(mode)
113
-
114
- mode
115
- else
116
- raise ArgumentError, "Mode must be a Symbol or Boolean, got: #{mode.class}"
117
- end
118
- end
119
-
120
- # Collapse 3+ consecutive newlines to 2.
121
- #
122
- # This detects runs of blank lines (empty lines) and collapses them.
123
- # Note: A blank line is a line containing only whitespace.
124
- # 3+ consecutive newlines means 2+ blank lines.
125
- #
126
- # @param text [String] Text to process
127
- # @return [String] Processed text
128
- def collapse_excessive_blank_lines(text)
129
- lines = text.lines
130
- result = []
131
- consecutive_blank_count = 0
132
- problem_start_line = nil
133
- line_number = 0
134
-
135
- lines.each do |line|
136
- line_number += 1
137
-
138
- if line.chomp.empty?
139
- consecutive_blank_count += 1
140
- # The problem starts at the line BEFORE the first blank line
141
- # (i.e., the line that ends with the first \n of the excessive sequence)
142
- problem_start_line ||= line_number - 1
143
-
144
- # Only add up to 1 blank line (which creates the standard paragraph gap)
145
- if consecutive_blank_count <= 1
146
- result << line
147
- end
148
- # Skip adding lines when consecutive_blank_count >= 2
149
- else
150
- # Record problem if we had 2+ blank lines (which means 3+ newlines)
151
- # consecutive_blank_count is the count of blank lines, so >= 2 means excessive
152
- if consecutive_blank_count >= 2
153
- @problems.add(
154
- :excessive_whitespace,
155
- severity: :warning,
156
- line: problem_start_line,
157
- newline_count: consecutive_blank_count + 1, # +1 because first line ends with \n too
158
- collapsed_to: 2,
159
- )
160
- end
161
-
162
- consecutive_blank_count = 0
163
- problem_start_line = nil
164
- result << line
165
- end
166
- end
167
-
168
- # Handle trailing blank lines
169
- if consecutive_blank_count >= 2
170
- @problems.add(
171
- :excessive_whitespace,
172
- severity: :warning,
173
- line: problem_start_line,
174
- newline_count: consecutive_blank_count + 1,
175
- collapsed_to: 2,
176
- )
177
- end
178
-
179
- result.join
180
- end
181
-
182
- # Remove blank lines between consecutive link reference definitions.
183
- #
184
- # Uses {LinkParser} to detect link definitions, supporting:
185
- # - Standard: `[label]: url`
186
- # - With title: `[label]: url "title"`
187
- # - Angle-bracketed: `[label]: <url>`
188
- # - Emoji labels: `[🎨logo]: url`
189
- #
190
- # @param text [String] Text to process
191
- # @return [String] Processed text
192
- def remove_blank_lines_between_link_refs(text)
193
- lines = text.lines
194
- result = []
195
- i = 0
196
-
197
- while i < lines.length
198
- line = lines[i]
199
- result << line
200
-
201
- # Check if current line is a link ref definition using LinkParser
202
- if link_definition_line?(line)
203
- # Look ahead for blank lines followed by another link ref
204
- j = i + 1
205
- while j < lines.length
206
- next_line = lines[j]
207
- if next_line.chomp.empty?
208
- # Check if there's a link ref definition after the blank line(s)
209
- k = j + 1
210
- while k < lines.length && lines[k].chomp.empty?
211
- k += 1
212
- end
213
- if k < lines.length && link_definition_line?(lines[k])
214
- # Skip all blank lines between link refs
215
- blanks_skipped = k - j
216
- @problems.add(
217
- :link_ref_spacing,
218
- severity: :info,
219
- line: j + 1,
220
- blank_lines_removed: blanks_skipped,
221
- )
222
- j = k
223
- else
224
- # Not followed by a link ref, keep the blank line
225
- break
226
- end
227
- else
228
- break
229
- end
230
- end
231
- i = j
232
- else
233
- i += 1
234
- end
235
- end
236
-
237
- result.join
238
- end
239
-
240
- # Check if a line is a link reference definition using LinkParser.
241
- #
242
- # @param line [String] Line to check
243
- # @return [Boolean] true if line is a link definition
244
- def link_definition_line?(line)
245
- # Use LinkParser to attempt parsing the line as a definition
246
- result = @link_parser.parse_definition_line(line.chomp)
247
- !result.nil?
248
- end
249
- end
250
- end
251
- end
@@ -1,341 +0,0 @@
1
- module Markdown
2
- module Merge
3
- VERSION: String
4
-
5
- # Base error class for Markdown::Merge
6
- class Error < Ast::Merge::Error
7
- end
8
-
9
- # Raised when a Markdown file has parsing errors
10
- class ParseError < Ast::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
- # Debug logging utility for Markdown::Merge operations
23
- module DebugLogger
24
- extend Ast::Merge::DebugLogger
25
-
26
- def self.env_var_name: () -> String
27
- def self.env_var_name=: (String name) -> String
28
- def self.log_prefix: () -> String
29
- def self.log_prefix=: (String prefix) -> String
30
- def self.enabled?: () -> bool
31
- def self.debug: (*untyped args) -> void
32
- def self.info: (*untyped args) -> void
33
- def self.warning: (*untyped args) -> void
34
- def self.time: [T] (String label) { () -> T } -> T
35
- def self.log_node: (untyped node, ?label: String) -> void
36
- end
37
-
38
- # Represents a frozen block of Markdown content
39
- class FreezeNode < Ast::Merge::FreezeNodeBase
40
- def initialize: (
41
- start_line: Integer,
42
- end_line: Integer,
43
- content: String,
44
- start_marker: String,
45
- end_marker: String,
46
- ?nodes: Array[untyped],
47
- ?reason: String?
48
- ) -> void
49
-
50
- def signature: () -> Array[Symbol | String]
51
- def full_text: () -> String
52
- def line_count: () -> Integer
53
- def contains_type?: (Symbol type_symbol) -> bool
54
- def inspect: () -> String
55
- end
56
-
57
- # Base class for file analysis for Markdown files
58
- class FileAnalysisBase
59
- include Ast::Merge::FileAnalyzable
60
-
61
- DEFAULT_FREEZE_TOKEN: String
62
-
63
- attr_reader document: untyped
64
- attr_reader statements: Array[untyped]
65
-
66
- def initialize: (
67
- String source,
68
- ?freeze_token: String,
69
- ?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
70
- **untyped parser_options
71
- ) -> void
72
-
73
- # Abstract methods - subclasses must implement
74
- def parse_document: (String source) -> untyped
75
- def next_sibling: (untyped node) -> untyped?
76
-
77
- def valid?: () -> bool
78
- def compute_node_signature: (untyped node) -> Array[untyped]?
79
- def fallthrough_node?: (untyped value) -> bool
80
- def parser_node?: (untyped value) -> bool
81
- def compute_parser_signature: (untyped node) -> Array[untyped]?
82
- def safe_string_content: (untyped node) -> String
83
- def extract_text_content: (untyped node) -> String
84
- def extract_table_header_content: (untyped node) -> String
85
- def source_range: (Integer start_line, Integer end_line) -> String
86
- def count_children: (untyped node) -> Integer
87
-
88
- private
89
-
90
- def extract_and_integrate_all_nodes: () -> Array[untyped]
91
- def collect_top_level_nodes: () -> Array[untyped]
92
- def build_freeze_blocks: (Array[untyped] nodes) -> Array[FreezeNode]
93
- def integrate_freeze_blocks: (Array[untyped] nodes, Array[FreezeNode] freeze_blocks) -> Array[untyped]
94
- end
95
-
96
- # Aligns Markdown block elements between template and destination files
97
- class FileAligner
98
- attr_reader template_analysis: FileAnalysisBase
99
- attr_reader dest_analysis: FileAnalysisBase
100
- attr_reader match_refiner: (^(Array[untyped], Array[untyped], Hash[Symbol, untyped]) -> Array[MatchResult])?
101
-
102
- def initialize: (
103
- FileAnalysisBase template_analysis,
104
- FileAnalysisBase dest_analysis,
105
- ?match_refiner: (^(Array[untyped], Array[untyped], Hash[Symbol, untyped]) -> Array[MatchResult])?
106
- ) -> void
107
-
108
- def align: () -> Array[Hash[Symbol, untyped]]
109
-
110
- private
111
-
112
- def build_signature_map: (Array[untyped] statements, FileAnalysisBase analysis) -> Hash[Array[untyped], Array[Integer]]
113
- end
114
-
115
- # Match result from refiners
116
- class MatchResult
117
- attr_reader template_node: untyped
118
- attr_reader dest_node: untyped
119
- attr_reader score: Float
120
-
121
- def initialize: (
122
- template_node: untyped,
123
- dest_node: untyped,
124
- score: Float
125
- ) -> void
126
- end
127
-
128
- # Resolves conflicts between matching Markdown elements
129
- class ConflictResolver < Ast::Merge::ConflictResolverBase
130
- def initialize: (
131
- preference: Symbol,
132
- template_analysis: FileAnalysisBase,
133
- dest_analysis: FileAnalysisBase
134
- ) -> void
135
-
136
- private
137
-
138
- def resolve_node_pair: (
139
- untyped template_node,
140
- untyped dest_node,
141
- template_index: Integer,
142
- dest_index: Integer
143
- ) -> Hash[Symbol, untyped]
144
-
145
- def content_identical?: (untyped template_node, untyped dest_node) -> bool
146
- def node_to_text: (untyped node, FileAnalysisBase analysis) -> String
147
- end
148
-
149
- # Represents the result of a Markdown merge operation
150
- class MergeResult < Ast::Merge::MergeResultBase
151
- def initialize: (
152
- content: String?,
153
- ?conflicts: Array[Hash[Symbol, untyped]],
154
- ?frozen_blocks: Array[Hash[Symbol, untyped]],
155
- ?stats: Hash[Symbol, untyped]
156
- ) -> void
157
-
158
- def content: () -> String?
159
- def content?: () -> bool
160
- def content_string: () -> String?
161
- def success?: () -> bool
162
- def conflicts?: () -> bool
163
- def has_frozen_blocks?: () -> bool
164
- def nodes_added: () -> Integer
165
- def nodes_removed: () -> Integer
166
- def nodes_modified: () -> Integer
167
- def merge_time_ms: () -> Float?
168
- def frozen_count: () -> Integer
169
- def inspect: () -> String
170
- def to_s: () -> String
171
-
172
- private
173
-
174
- def default_stats: () -> Hash[Symbol, untyped]
175
- end
176
-
177
- # Algorithm for computing match scores between two Markdown tables
178
- class TableMatchAlgorithm
179
- DEFAULT_WEIGHTS: Hash[Symbol, Float]
180
- FIRST_COLUMN_SIMILARITY_THRESHOLD: Float
181
-
182
- attr_reader position_a: Integer?
183
- attr_reader position_b: Integer?
184
- attr_reader total_tables_a: Integer
185
- attr_reader total_tables_b: Integer
186
- attr_reader weights: Hash[Symbol, Float]
187
-
188
- def initialize: (
189
- ?position_a: Integer?,
190
- ?position_b: Integer?,
191
- ?total_tables_a: Integer,
192
- ?total_tables_b: Integer,
193
- ?weights: Hash[Symbol, Float]
194
- ) -> void
195
-
196
- def call: (untyped table_a, untyped table_b) -> Float
197
-
198
- private
199
-
200
- def levenshtein_distance: (String a, String b) -> Integer
201
- def levenshtein_similarity: (String a, String b) -> Float
202
- def extract_rows: (untyped table) -> Array[Array[String]]
203
- def extract_cell_text: (untyped cell) -> String
204
- def compute_header_match: (Array[Array[String]] rows_a, Array[Array[String]] rows_b) -> Float
205
- def compute_first_column_match: (Array[Array[String]] rows_a, Array[Array[String]] rows_b) -> Float
206
- def compute_row_content_match: (Array[Array[String]] rows_a, Array[Array[String]] rows_b) -> Float
207
- def compute_total_cells_match: (Array[Array[String]] rows_a, Array[Array[String]] rows_b) -> Float
208
- def compute_position_score: () -> Float
209
- def weighted_average: (Hash[Symbol, Float] scores) -> Float
210
- end
211
-
212
- # Match refiner for Markdown tables that didn't match by exact signature
213
- class TableMatchRefiner < Ast::Merge::MatchRefinerBase
214
- attr_reader algorithm_options: Hash[Symbol, untyped]
215
-
216
- def initialize: (
217
- ?threshold: Float,
218
- ?algorithm_options: Hash[Symbol, untyped],
219
- **untyped options
220
- ) -> void
221
-
222
- def call: (
223
- Array[untyped] template_nodes,
224
- Array[untyped] dest_nodes,
225
- ?Hash[Symbol, untyped] context
226
- ) -> Array[MatchResult]
227
-
228
- private
229
-
230
- def extract_tables: (Array[untyped] nodes) -> Array[untyped]
231
- def table_node?: (untyped node) -> bool
232
- def compute_table_similarity: (
233
- untyped t_table,
234
- untyped d_table,
235
- Integer t_idx,
236
- Integer d_idx,
237
- Integer total_t,
238
- Integer total_d
239
- ) -> Float
240
- end
241
-
242
- # Merges fenced code blocks using language-specific *-merge gems
243
- class CodeBlockMerger
244
- DEFAULT_MERGERS: Hash[String, ^(String, String, Symbol, **untyped) -> Hash[Symbol, untyped]]
245
-
246
- attr_reader mergers: Hash[String, ^(String, String, Symbol, **untyped) -> Hash[Symbol, untyped]]
247
- attr_reader enabled: bool
248
-
249
- def initialize: (
250
- ?mergers: Hash[String, ^(String, String, Symbol, **untyped) -> Hash[Symbol, untyped]],
251
- ?enabled: bool
252
- ) -> void
253
-
254
- def supports_language?: (String? language) -> bool
255
- def merge_code_blocks: (
256
- untyped template_node,
257
- untyped dest_node,
258
- preference: Symbol,
259
- **untyped opts
260
- ) -> Hash[Symbol, untyped]
261
-
262
- # Class methods for specific mergers
263
- def self.merge_with_prism: (String template, String dest, Symbol preference, **untyped opts) -> Hash[Symbol, untyped]
264
- def self.merge_with_psych: (String template, String dest, Symbol preference, **untyped opts) -> Hash[Symbol, untyped]
265
- def self.merge_with_json: (String template, String dest, Symbol preference, **untyped opts) -> Hash[Symbol, untyped]
266
- def self.merge_with_toml: (String template, String dest, Symbol preference, **untyped opts) -> Hash[Symbol, untyped]
267
-
268
- private
269
-
270
- def extract_language: (untyped node) -> String?
271
- def extract_content: (untyped node) -> String
272
- def rebuild_code_block: (String language, String content, untyped reference_node) -> String
273
- def not_merged: (String reason) -> Hash[Symbol, untyped]
274
- end
275
-
276
- # Builds markdown output from merge operations
277
- class OutputBuilder
278
- @parts: Array[String]
279
- @preserve_formatting: bool
280
-
281
- def initialize: (?preserve_formatting: bool) -> void
282
- def add_node_source: (untyped node, FileAnalysisBase analysis) -> void
283
- def add_link_definition: (LinkDefinitionNode node) -> void
284
- def add_gap_line: (?count: Integer) -> void
285
- def add_raw: (String text) -> void
286
- def to_s: () -> String
287
- def empty?: () -> bool
288
- def clear: () -> void
289
-
290
- private
291
-
292
- def extract_source: (untyped node, FileAnalysisBase analysis) -> String?
293
- def extract_parser_node_source: (untyped node, FileAnalysisBase analysis) -> String?
294
- end
295
-
296
- # Formats link reference definitions for output
297
- module LinkDefinitionFormatter
298
- def self.format: (LinkDefinitionNode node) -> String
299
- def self.format_all: (Array[LinkDefinitionNode] nodes, ?separator: String) -> String
300
- end
301
-
302
- # Base class for smart Markdown file merging
303
- class SmartMergerBase
304
- attr_reader template_analysis: FileAnalysisBase
305
- attr_reader dest_analysis: FileAnalysisBase
306
- attr_reader aligner: FileAligner
307
- attr_reader resolver: ConflictResolver
308
- attr_reader code_block_merger: CodeBlockMerger?
309
-
310
- def initialize: (
311
- String template_content,
312
- String dest_content,
313
- ?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
314
- ?preference: Symbol,
315
- ?add_template_only_nodes: bool,
316
- ?inner_merge_code_blocks: (bool | CodeBlockMerger),
317
- ?freeze_token: String,
318
- ?match_refiner: (^(Array[untyped], Array[untyped], Hash[Symbol, untyped]) -> Array[MatchResult])?,
319
- **untyped parser_options
320
- ) -> void
321
-
322
- # Abstract methods - subclasses must implement
323
- def create_file_analysis: (String content, **untyped options) -> FileAnalysisBase
324
- def node_to_source: (untyped node, FileAnalysisBase analysis) -> String
325
-
326
- def template_parse_error_class: () -> Class
327
- def destination_parse_error_class: () -> Class
328
-
329
- def merge: () -> String
330
- def merge_result: () -> MergeResult
331
-
332
- private
333
-
334
- def build_result_content: (Array[Hash[Symbol, untyped]] alignment) -> String
335
- def process_alignment_entry: (Hash[Symbol, untyped] entry) -> String?
336
- def handle_match: (Hash[Symbol, untyped] entry) -> String?
337
- def handle_template_only: (Hash[Symbol, untyped] entry) -> String?
338
- def handle_dest_only: (Hash[Symbol, untyped] entry) -> String?
339
- end
340
- end
341
- end