markdown-merge 7.0.0 → 7.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +673 -0
- data/lib/markdown/merge/backend_support.rb +200 -0
- data/lib/markdown/merge/cleanse/block_spacing.rb +248 -0
- data/lib/markdown/merge/cleanse/code_fence_spacing.rb +294 -0
- data/lib/markdown/merge/cleanse/condensed_link_refs.rb +411 -0
- data/lib/markdown/merge/cleanse/list_marker_duplication.rb +66 -0
- data/lib/markdown/merge/cleanse/templating_corruption.rb +86 -0
- data/lib/markdown/merge/cleanse.rb +44 -0
- data/lib/markdown/merge/code_block_match_refiner.rb +111 -0
- data/lib/markdown/merge/code_block_merger.rb +742 -0
- data/lib/markdown/merge/comment_tracker.rb +42 -0
- data/lib/markdown/merge/conflict_resolver.rb +199 -0
- data/lib/markdown/merge/debug_logger.rb +26 -0
- data/lib/markdown/merge/document_problems.rb +190 -0
- data/lib/markdown/merge/file_aligner.rb +496 -0
- data/lib/markdown/merge/file_analysis.rb +689 -0
- data/lib/markdown/merge/file_analysis_base.rb +766 -0
- data/lib/markdown/merge/freeze_node.rb +93 -0
- data/lib/markdown/merge/gap_line_node.rb +142 -0
- data/lib/markdown/merge/link_definition_formatter.rb +49 -0
- data/lib/markdown/merge/link_definition_node.rb +157 -0
- data/lib/markdown/merge/link_parser.rb +421 -0
- data/lib/markdown/merge/link_reference_rehydrator.rb +320 -0
- data/lib/markdown/merge/list_match_refiner.rb +98 -0
- data/lib/markdown/merge/list_merger.rb +322 -0
- data/lib/markdown/merge/markdown_structure.rb +123 -0
- data/lib/markdown/merge/merge_result.rb +483 -0
- data/lib/markdown/merge/node_type_normalizer.rb +126 -0
- data/lib/markdown/merge/output_builder.rb +248 -0
- data/lib/markdown/merge/partial_template_merger.rb +555 -0
- data/lib/markdown/merge/preservation_support.rb +291 -0
- data/lib/markdown/merge/rspec/shared_examples/source_preserving_provider.rb +338 -0
- data/lib/markdown/merge/smart_merger.rb +269 -0
- data/lib/markdown/merge/smart_merger_base.rb +1490 -0
- data/lib/markdown/merge/source_preserving_provider.rb +814 -0
- data/lib/markdown/merge/table_match_algorithm.rb +499 -0
- data/lib/markdown/merge/table_match_refiner.rb +132 -0
- data/lib/markdown/merge/version.rb +5 -3
- data/lib/markdown/merge/whitespace_normalizer.rb +243 -0
- data/lib/markdown/merge/wrapper_support.rb +194 -0
- data/lib/markdown/merge.rb +271 -87
- data/lib/markdown-merge.rb +7 -1
- data/sig/markdown/merge.rbs +62 -0
- data.tar.gz.sig +0 -0
- metadata +289 -15
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdown
|
|
4
|
+
module Merge
|
|
5
|
+
# Shared boilerplate for backend-specific Markdown wrapper backends.
|
|
6
|
+
module BackendSupport
|
|
7
|
+
DEFAULT_CAPABILITIES = {
|
|
8
|
+
query: false,
|
|
9
|
+
bytes_field: false,
|
|
10
|
+
incremental: false,
|
|
11
|
+
pure_ruby: false,
|
|
12
|
+
markdown_only: true,
|
|
13
|
+
error_tolerant: true
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def install!(backend_module:, backend_name:, gem_name:, require_path:, capabilities: {})
|
|
19
|
+
install_availability_methods!(
|
|
20
|
+
backend_module: backend_module,
|
|
21
|
+
backend_name: backend_name,
|
|
22
|
+
gem_name: gem_name,
|
|
23
|
+
capabilities: capabilities
|
|
24
|
+
)
|
|
25
|
+
install_tree_wrapper!(backend_module)
|
|
26
|
+
register_backend!(
|
|
27
|
+
backend_module: backend_module,
|
|
28
|
+
backend_name: backend_name,
|
|
29
|
+
gem_name: gem_name,
|
|
30
|
+
require_path: require_path
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def configure_markdown_only_language_class!(klass, backend_label:, factory_method: :markdown,
|
|
35
|
+
unsupported_language_message: nil)
|
|
36
|
+
klass.singleton_class.class_eval do
|
|
37
|
+
define_method(:from_library) do |_path = nil, symbol: nil, name: nil|
|
|
38
|
+
lang_name = name || symbol&.to_s&.sub(/^tree_sitter_/, '')&.to_sym || :markdown
|
|
39
|
+
|
|
40
|
+
unless lang_name == :markdown
|
|
41
|
+
message = if unsupported_language_message.respond_to?(:call)
|
|
42
|
+
unsupported_language_message.call(lang_name)
|
|
43
|
+
else
|
|
44
|
+
unsupported_language_message || "#{backend_label} backend only supports Markdown, not #{lang_name}."
|
|
45
|
+
end
|
|
46
|
+
raise TreeHaver::NotAvailable, message
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
public_send(factory_method)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def configure_node_link_and_navigation!(klass, next_sibling_selector:, prev_sibling_selector:,
|
|
55
|
+
parent_selector: :parent)
|
|
56
|
+
klass.class_eval do
|
|
57
|
+
define_method(:url) do
|
|
58
|
+
Markdown::Merge::BackendSupport.send(:safe_inner_node_call, inner_node, :url)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
define_method(:title) do
|
|
62
|
+
Markdown::Merge::BackendSupport.send(:safe_inner_node_call, inner_node, :title)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
define_method(:next_sibling) do
|
|
66
|
+
sibling = Markdown::Merge::BackendSupport.send(:safe_inner_node_call, inner_node, next_sibling_selector)
|
|
67
|
+
sibling ? self.class.new(sibling, source: source, lines: lines) : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
define_method(:prev_sibling) do
|
|
71
|
+
sibling = Markdown::Merge::BackendSupport.send(:safe_inner_node_call, inner_node, prev_sibling_selector)
|
|
72
|
+
sibling ? self.class.new(sibling, source: source, lines: lines) : nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
define_method(:parent) do
|
|
76
|
+
parent_node = Markdown::Merge::BackendSupport.send(:safe_inner_node_call, inner_node, parent_selector)
|
|
77
|
+
parent_node ? self.class.new(parent_node, source: source, lines: lines) : nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def configure_node_heading_and_code_block_helpers!(klass, heading_matcher:, code_block_matcher:,
|
|
83
|
+
heading_level_extractor: nil,
|
|
84
|
+
code_block_info_extractor: nil)
|
|
85
|
+
klass.class_eval do
|
|
86
|
+
define_method(:header_level) do
|
|
87
|
+
return unless Markdown::Merge::BackendSupport.send(:node_helper_match?, self, heading_matcher)
|
|
88
|
+
|
|
89
|
+
if heading_level_extractor.respond_to?(:call)
|
|
90
|
+
heading_level_extractor.call(inner_node)
|
|
91
|
+
else
|
|
92
|
+
Markdown::Merge::BackendSupport.send(:safe_inner_node_call, inner_node, :header_level)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
define_method(:fence_info) do
|
|
97
|
+
return unless Markdown::Merge::BackendSupport.send(:node_helper_match?, self, code_block_matcher)
|
|
98
|
+
|
|
99
|
+
if code_block_info_extractor.respond_to?(:call)
|
|
100
|
+
code_block_info_extractor.call(inner_node)
|
|
101
|
+
else
|
|
102
|
+
Markdown::Merge::BackendSupport.send(:safe_inner_node_call, inner_node, :fence_info)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def install_availability_methods!(backend_module:, backend_name:, gem_name:, capabilities: {})
|
|
109
|
+
backend_module.instance_variable_set(:@load_attempted, false)
|
|
110
|
+
backend_module.instance_variable_set(:@loaded, false)
|
|
111
|
+
|
|
112
|
+
merged_capabilities = DEFAULT_CAPABILITIES.merge(backend: backend_name).merge(capabilities)
|
|
113
|
+
|
|
114
|
+
backend_module.singleton_class.class_eval do
|
|
115
|
+
define_method(:available?) do
|
|
116
|
+
return @loaded if @load_attempted
|
|
117
|
+
|
|
118
|
+
@load_attempted = true
|
|
119
|
+
begin
|
|
120
|
+
require gem_name
|
|
121
|
+
@loaded = true
|
|
122
|
+
rescue LoadError, StandardError
|
|
123
|
+
@loaded = false
|
|
124
|
+
end
|
|
125
|
+
@loaded
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
define_method(:reset!) do
|
|
129
|
+
@load_attempted = false
|
|
130
|
+
@loaded = false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
define_method(:capabilities) do
|
|
134
|
+
return {} unless available?
|
|
135
|
+
|
|
136
|
+
merged_capabilities.dup
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
private_class_method :install_availability_methods!
|
|
141
|
+
|
|
142
|
+
def install_tree_wrapper!(backend_module)
|
|
143
|
+
return if backend_module.const_defined?(:Tree, false)
|
|
144
|
+
|
|
145
|
+
tree_class = Class.new(::TreeHaver::Base::Tree) do
|
|
146
|
+
define_method(:initialize) do |document, source|
|
|
147
|
+
super(document, source: source)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
define_method(:root_node) do
|
|
151
|
+
backend_module.const_get(:Node).new(inner_tree, source: source, lines: lines)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
backend_module.const_set(:Tree, tree_class)
|
|
156
|
+
backend_module.const_set(:Point, ::TreeHaver::Base::Point) unless backend_module.const_defined?(:Point, false)
|
|
157
|
+
end
|
|
158
|
+
private_class_method :install_tree_wrapper!
|
|
159
|
+
|
|
160
|
+
def register_backend!(backend_module:, backend_name:, gem_name:, require_path:)
|
|
161
|
+
::TreeHaver::BackendRegistry.register(
|
|
162
|
+
::TreeHaver::BackendReference.new(id: backend_name.to_s, family: 'native')
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
::TreeHaver.register_language(
|
|
166
|
+
:markdown,
|
|
167
|
+
backend_type: backend_name,
|
|
168
|
+
backend_module: backend_module,
|
|
169
|
+
gem_name: gem_name
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if ::TreeHaver::BackendRegistry.respond_to?(:register_tag)
|
|
173
|
+
::TreeHaver::BackendRegistry.register_tag(
|
|
174
|
+
:"#{backend_name}_backend",
|
|
175
|
+
category: :backend,
|
|
176
|
+
backend_name: backend_name,
|
|
177
|
+
require_path: require_path
|
|
178
|
+
) { backend_module.available? }
|
|
179
|
+
else
|
|
180
|
+
::TreeHaver::BackendRegistry.register_availability_checker(backend_name) do
|
|
181
|
+
backend_module.available?
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
private_class_method :register_backend!
|
|
186
|
+
|
|
187
|
+
def safe_inner_node_call(node, method_name)
|
|
188
|
+
node.public_send(method_name)
|
|
189
|
+
rescue StandardError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
private_class_method :safe_inner_node_call
|
|
193
|
+
|
|
194
|
+
def node_helper_match?(node, matcher)
|
|
195
|
+
matcher.respond_to?(:call) ? matcher.call(node) : false
|
|
196
|
+
end
|
|
197
|
+
private_class_method :node_helper_match?
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parslet'
|
|
4
|
+
|
|
5
|
+
module Markdown
|
|
6
|
+
module Merge
|
|
7
|
+
module Cleanse
|
|
8
|
+
# Fixes missing blank lines between block elements in Markdown.
|
|
9
|
+
#
|
|
10
|
+
# Markdown best practices require blank lines between:
|
|
11
|
+
# - List items and headings
|
|
12
|
+
# - Thematic breaks (---) and following content
|
|
13
|
+
# - HTML blocks (like </details>) and following markdown
|
|
14
|
+
# - Nested list items and headings
|
|
15
|
+
#
|
|
16
|
+
# This class detects and fixes these issues without using a full
|
|
17
|
+
# markdown parser, making it safe to use on documents that might
|
|
18
|
+
# have syntax issues.
|
|
19
|
+
#
|
|
20
|
+
# @example Basic usage
|
|
21
|
+
# fixer = Markdown::Merge::Cleanse::BlockSpacing.new(content)
|
|
22
|
+
# if fixer.malformed?
|
|
23
|
+
# fixed_content = fixer.fix
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Check specific issues
|
|
27
|
+
# fixer = Markdown::Merge::Cleanse::BlockSpacing.new(content)
|
|
28
|
+
# fixer.issues.each do |issue|
|
|
29
|
+
# puts "Line #{issue[:line]}: #{issue[:type]}"
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class BlockSpacing
|
|
33
|
+
# Patterns for block elements that should have blank lines after them
|
|
34
|
+
THEMATIC_BREAK = /\A\s*(?:---+|\*\*\*+|___+)\s*\z/
|
|
35
|
+
HEADING = /\A\s*\#{1,6}\s+/
|
|
36
|
+
LIST_ITEM = /\A\s*(?:[-*+]|\d+\.)\s+/
|
|
37
|
+
HTML_CLOSE_TAG = %r{\A\s*</[a-zA-Z][a-zA-Z0-9]*>\s*\z}
|
|
38
|
+
HTML_OPEN_TAG = /\A\s*<[a-zA-Z][a-zA-Z0-9]*(?:\s|>)/
|
|
39
|
+
HTML_ANY_TAG = %r{\A\s*</?[a-zA-Z]}
|
|
40
|
+
LINK_REF_DEF = /\A\s*\[[^\]]+\]:\s*/
|
|
41
|
+
|
|
42
|
+
# Block-level HTML elements that can span multiple lines
|
|
43
|
+
# These create a context where we shouldn't insert blank lines
|
|
44
|
+
HTML_BLOCK_ELEMENTS = %w[
|
|
45
|
+
ul
|
|
46
|
+
ol
|
|
47
|
+
li
|
|
48
|
+
dl
|
|
49
|
+
dt
|
|
50
|
+
dd
|
|
51
|
+
div
|
|
52
|
+
table
|
|
53
|
+
thead
|
|
54
|
+
tbody
|
|
55
|
+
tfoot
|
|
56
|
+
tr
|
|
57
|
+
th
|
|
58
|
+
td
|
|
59
|
+
blockquote
|
|
60
|
+
pre
|
|
61
|
+
figure
|
|
62
|
+
figcaption
|
|
63
|
+
details
|
|
64
|
+
summary
|
|
65
|
+
section
|
|
66
|
+
article
|
|
67
|
+
aside
|
|
68
|
+
nav
|
|
69
|
+
header
|
|
70
|
+
footer
|
|
71
|
+
main
|
|
72
|
+
address
|
|
73
|
+
form
|
|
74
|
+
fieldset
|
|
75
|
+
].freeze
|
|
76
|
+
|
|
77
|
+
# Pattern to match opening block-level HTML tags
|
|
78
|
+
HTML_BLOCK_OPEN = /\A\s*<(#{HTML_BLOCK_ELEMENTS.join('|')})(?:\s|>)/i
|
|
79
|
+
|
|
80
|
+
# Pattern to match closing block-level HTML tags
|
|
81
|
+
HTML_BLOCK_CLOSE = %r{\A\s*</(#{HTML_BLOCK_ELEMENTS.join('|')})>}i
|
|
82
|
+
|
|
83
|
+
# HTML elements that contain markdown content (not HTML content)
|
|
84
|
+
# These should have blank lines before their closing tags
|
|
85
|
+
MARKDOWN_CONTAINER_ELEMENTS = %w[details].freeze
|
|
86
|
+
|
|
87
|
+
# Pattern to match closing tags for markdown containers
|
|
88
|
+
MARKDOWN_CONTAINER_CLOSE = %r{\A\s*</(#{MARKDOWN_CONTAINER_ELEMENTS.join('|')})>}i
|
|
89
|
+
|
|
90
|
+
# Markdown content: anything that's not blank, not HTML, and not a link ref def
|
|
91
|
+
MARKDOWN_CONTENT = lambda { |line|
|
|
92
|
+
stripped = line.strip
|
|
93
|
+
return false if stripped.empty?
|
|
94
|
+
return false if stripped.start_with?('<')
|
|
95
|
+
return false if line.match?(LINK_REF_DEF)
|
|
96
|
+
|
|
97
|
+
true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# @return [String] The original content
|
|
101
|
+
attr_reader :source
|
|
102
|
+
|
|
103
|
+
# @return [Array<Hash>] Issues found
|
|
104
|
+
attr_reader :issues
|
|
105
|
+
|
|
106
|
+
# Initialize a new BlockSpacing fixer.
|
|
107
|
+
#
|
|
108
|
+
# @param source [String] The markdown content to analyze
|
|
109
|
+
def initialize(source)
|
|
110
|
+
@source = source
|
|
111
|
+
@issues = []
|
|
112
|
+
analyze
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if the content has block spacing issues.
|
|
116
|
+
#
|
|
117
|
+
# @return [Boolean] true if issues were found
|
|
118
|
+
def malformed?
|
|
119
|
+
@issues.any?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get the count of issues found.
|
|
123
|
+
#
|
|
124
|
+
# @return [Integer] number of issues
|
|
125
|
+
def issue_count
|
|
126
|
+
@issues.size
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Fix the block spacing issues.
|
|
130
|
+
#
|
|
131
|
+
# @return [String] Content with blank lines added where needed
|
|
132
|
+
def fix
|
|
133
|
+
return source unless malformed?
|
|
134
|
+
|
|
135
|
+
lines = source.lines
|
|
136
|
+
result = []
|
|
137
|
+
insertions = @issues.map { |i| i[:line] }.to_set
|
|
138
|
+
|
|
139
|
+
lines.each_with_index do |line, idx|
|
|
140
|
+
result << line
|
|
141
|
+
# If this line needs a blank line after it, add one
|
|
142
|
+
result << "\n" if insertions.include?(idx + 1) && !line.strip.empty? # issues use 1-based line numbers
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
result.join
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def analyze
|
|
151
|
+
lines = source.lines
|
|
152
|
+
return if lines.empty?
|
|
153
|
+
|
|
154
|
+
# Track depth of block-level HTML elements
|
|
155
|
+
# When depth > 0, we're inside an HTML block and shouldn't add blank lines
|
|
156
|
+
html_block_depth = 0
|
|
157
|
+
|
|
158
|
+
lines.each_with_index do |line, idx|
|
|
159
|
+
next_line = lines[idx + 1]
|
|
160
|
+
prev_line = idx.positive? ? lines[idx - 1] : nil
|
|
161
|
+
|
|
162
|
+
# Special case: closing tags for markdown containers like </details>
|
|
163
|
+
# These contain markdown content, so we need blank lines before them
|
|
164
|
+
# even when inside an HTML block
|
|
165
|
+
is_markdown_container_close = line.match?(MARKDOWN_CONTAINER_CLOSE)
|
|
166
|
+
|
|
167
|
+
# Check for issues BEFORE updating depth
|
|
168
|
+
if html_block_depth <= 0
|
|
169
|
+
# Check for issues that need blank line AFTER current line
|
|
170
|
+
if next_line && !next_line.strip.empty?
|
|
171
|
+
check_thematic_break(line, next_line, idx)
|
|
172
|
+
check_list_before_heading(line, next_line, idx)
|
|
173
|
+
check_html_close_before_markdown(line, next_line, idx)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Check for issues that need blank line BEFORE current line
|
|
177
|
+
check_markdown_before_html(prev_line, line, idx) if prev_line && !prev_line.strip.empty?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Special case: always check for blank line before </details> etc.
|
|
181
|
+
# because they contain markdown content
|
|
182
|
+
if is_markdown_container_close && prev_line && !prev_line.strip.empty?
|
|
183
|
+
check_markdown_before_html(prev_line, line, idx)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Update HTML block depth AFTER checking for issues
|
|
187
|
+
# Count opening block-level tags
|
|
188
|
+
html_block_depth += 1 if line.match?(HTML_BLOCK_OPEN)
|
|
189
|
+
|
|
190
|
+
# Check for closing block-level tags
|
|
191
|
+
line.scan(HTML_BLOCK_CLOSE) do
|
|
192
|
+
html_block_depth -= 1 if html_block_depth.positive?
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def check_thematic_break(line, next_line, idx)
|
|
198
|
+
return unless line.match?(THEMATIC_BREAK)
|
|
199
|
+
return if next_line.strip.empty?
|
|
200
|
+
|
|
201
|
+
@issues << {
|
|
202
|
+
type: :thematic_break_needs_blank,
|
|
203
|
+
line: idx + 1,
|
|
204
|
+
description: 'Thematic break should be followed by blank line'
|
|
205
|
+
}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def check_list_before_heading(line, next_line, idx)
|
|
209
|
+
return unless line.match?(LIST_ITEM)
|
|
210
|
+
return unless next_line.match?(HEADING)
|
|
211
|
+
|
|
212
|
+
@issues << {
|
|
213
|
+
type: :list_before_heading,
|
|
214
|
+
line: idx + 1,
|
|
215
|
+
description: 'List item should be followed by blank line before heading'
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def check_html_close_before_markdown(line, next_line, idx)
|
|
220
|
+
return unless line.match?(HTML_CLOSE_TAG)
|
|
221
|
+
# Next line is markdown (heading, list, paragraph start, etc.)
|
|
222
|
+
# but not HTML or blank
|
|
223
|
+
return if next_line.match?(/\A\s*</)
|
|
224
|
+
return if next_line.match?(LINK_REF_DEF)
|
|
225
|
+
|
|
226
|
+
@issues << {
|
|
227
|
+
type: :html_before_markdown,
|
|
228
|
+
line: idx + 1,
|
|
229
|
+
description: 'HTML close tag should be followed by blank line before markdown'
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def check_markdown_before_html(prev_line, line, idx)
|
|
234
|
+
# Current line is HTML (open or close tag)
|
|
235
|
+
return unless line.match?(HTML_ANY_TAG)
|
|
236
|
+
# Previous line is markdown content (not HTML, not blank, not link ref)
|
|
237
|
+
return unless MARKDOWN_CONTENT.call(prev_line)
|
|
238
|
+
|
|
239
|
+
@issues << {
|
|
240
|
+
type: :markdown_before_html,
|
|
241
|
+
line: idx, # Insert blank line BEFORE this line (so after prev_line)
|
|
242
|
+
description: 'Markdown content should be followed by blank line before HTML'
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|