blueprint-html2slim 1.1.0 → 1.3.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.
@@ -0,0 +1,286 @@
1
+ require_relative 'slim_manipulator'
2
+
3
+ module Blueprint
4
+ module Html2Slim
5
+ class SlimExtractor < SlimManipulator
6
+ def extract_file(file_path)
7
+ content = read_file(file_path)
8
+ structure = parse_slim_structure(content)
9
+
10
+ # Handle different extraction modes
11
+ sections_to_remove = []
12
+ sections_to_keep = []
13
+
14
+ extracted = if options[:outline]
15
+ extract_outline(structure, options[:outline])
16
+ elsif options[:selector]
17
+ extract_by_selector(structure, options[:selector])
18
+ else
19
+ # Original keep/remove logic
20
+ sections_to_remove = normalize_selectors(options[:remove] || [])
21
+ sections_to_keep = normalize_selectors(options[:keep] || [])
22
+
23
+ # Default removals if not keeping specific sections
24
+ if sections_to_keep.empty? && sections_to_remove.empty?
25
+ sections_to_remove = %w[doctype head nav header footer script]
26
+ end
27
+
28
+ # Extract content
29
+ extract_content(structure, sections_to_keep, sections_to_remove)
30
+ end
31
+
32
+ # Remove wrapper if requested (not for outline mode)
33
+ extracted = remove_outer_wrapper(extracted) if options[:remove_wrapper] && !options[:outline]
34
+
35
+ # Rebuild the Slim content
36
+ new_content = rebuild_extracted_content(extracted)
37
+
38
+ # Write to output file
39
+ output_path = options[:output] || file_path.sub(/\.slim$/, '_extracted.slim')
40
+ write_file(output_path, new_content)
41
+
42
+ # Build appropriate response based on extraction mode
43
+ if options[:outline]
44
+ {
45
+ success: true,
46
+ mode: 'outline',
47
+ depth: options[:outline]
48
+ }
49
+ elsif options[:selector]
50
+ {
51
+ success: true,
52
+ mode: 'selector',
53
+ selector: options[:selector]
54
+ }
55
+ else
56
+ {
57
+ success: true,
58
+ removed: sections_to_remove,
59
+ kept: sections_to_keep.empty? ? nil : sections_to_keep
60
+ }
61
+ end
62
+ rescue StandardError => e
63
+ { success: false, error: e.message }
64
+ end
65
+
66
+ private
67
+
68
+ def normalize_selectors(selectors)
69
+ return [] unless selectors
70
+
71
+ selectors.flat_map do |selector|
72
+ selector.split(',').map(&:strip).map(&:downcase)
73
+ end
74
+ end
75
+
76
+ def extract_content(structure, keep_selectors, remove_selectors)
77
+ result = []
78
+ skip_until_indent = nil
79
+ keep_until_indent = nil
80
+
81
+ structure.each_with_index do |item, _index|
82
+ # If we're in skip mode, check if we've exited the skipped section
83
+ if skip_until_indent
84
+ next if item[:indent_level] > skip_until_indent
85
+
86
+ skip_until_indent = nil
87
+ # Continue processing this line
88
+
89
+ end
90
+
91
+ # If we're in keep mode, track when we exit
92
+ keep_until_indent = nil if keep_until_indent && item[:indent_level] <= keep_until_indent
93
+
94
+ # Determine what to do with this line
95
+ if !keep_selectors.empty?
96
+ # We have keep selectors - only keep matching sections
97
+ if should_keep_line?(item, keep_selectors)
98
+ # This line matches a keep selector
99
+ keep_until_indent = item[:indent_level]
100
+ result << item
101
+ elsif keep_until_indent && item[:indent_level] > keep_until_indent
102
+ # We're inside a kept section
103
+ result << item
104
+ end
105
+ # Otherwise, skip this line
106
+ elsif should_remove_line?(item, remove_selectors)
107
+ # No keep selectors - use remove logic
108
+ skip_until_indent = item[:indent_level]
109
+ next
110
+ # Skip this line and all its children
111
+ else
112
+ result << item
113
+ end
114
+ end
115
+
116
+ result
117
+ end
118
+
119
+ def should_remove_line?(item, selectors)
120
+ return false if selectors.empty?
121
+
122
+ line = item[:stripped]
123
+
124
+ selectors.any? do |selector|
125
+ case selector
126
+ when 'doctype'
127
+ item[:type] == :doctype
128
+ when 'script', 'style', 'link', 'meta'
129
+ line.start_with?(selector)
130
+ else
131
+ # Check for element match or class/id match
132
+ element_info = element_selector(line)
133
+ if element_info
134
+ element_info[:element] == selector ||
135
+ element_info[:selector].include?(".#{selector}") ||
136
+ element_info[:selector].include?("##{selector}")
137
+ else
138
+ false
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ def should_keep_line?(item, selectors)
145
+ return true if selectors.empty?
146
+
147
+ line = item[:stripped]
148
+
149
+ selectors.any? do |selector|
150
+ element_info = element_selector(line)
151
+ if element_info
152
+ element_info[:element] == selector ||
153
+ element_info[:selector].include?(".#{selector}") ||
154
+ element_info[:selector].include?("##{selector}")
155
+ else
156
+ false
157
+ end
158
+ end
159
+ end
160
+
161
+ def remove_outer_wrapper(structure)
162
+ return structure if structure.empty?
163
+
164
+ # Find the minimum indentation level
165
+ min_indent = structure.map { |item| item[:indent_level] }.min
166
+
167
+ # If there's only one element at the minimum level, remove it
168
+ root_elements = structure.select { |item| item[:indent_level] == min_indent }
169
+
170
+ if root_elements.size == 1 && root_elements.first[:type] == :element
171
+ # Remove the wrapper and decrease indentation of all children
172
+ structure = structure[1..-1].map do |item|
173
+ item[:indent_level] -= 1 if item[:indent_level] > min_indent
174
+ item
175
+ end
176
+ end
177
+
178
+ structure
179
+ end
180
+
181
+ def rebuild_extracted_content(structure)
182
+ return '' if structure.empty?
183
+
184
+ # Normalize indentation - find minimum and adjust
185
+ min_indent = structure.map { |item| item[:indent_level] }.min || 0
186
+
187
+ structure.map do |item|
188
+ adjusted_indent = item[:indent_level] - min_indent
189
+ indent_string(adjusted_indent) + item[:stripped]
190
+ end.join("\n")
191
+ end
192
+
193
+ def extract_outline(structure, max_depth)
194
+ result = []
195
+
196
+ structure.each do |item|
197
+ # Include items up to the specified depth
198
+ result << item if item[:indent_level] < max_depth
199
+ end
200
+
201
+ result
202
+ end
203
+
204
+ def extract_by_selector(structure, selector)
205
+ result = []
206
+ in_selected_section = false
207
+ selected_indent = nil
208
+
209
+ # Parse the CSS selector
210
+ selector_parts = parse_css_selector(selector)
211
+
212
+ structure.each do |item|
213
+ # Check if we're exiting a selected section
214
+ if in_selected_section && selected_indent && item[:indent_level] <= selected_indent
215
+ in_selected_section = false
216
+ selected_indent = nil
217
+ end
218
+
219
+ # Check if this item matches the selector
220
+ if !in_selected_section && matches_selector?(item, selector_parts)
221
+ in_selected_section = true
222
+ selected_indent = item[:indent_level]
223
+ result << item
224
+ elsif in_selected_section
225
+ result << item
226
+ end
227
+ end
228
+
229
+ result
230
+ end
231
+
232
+ def parse_css_selector(selector)
233
+ # Support basic CSS selectors: element, #id, .class, element.class, element#id
234
+ parts = {}
235
+
236
+ # Handle complex selectors like div.container#main
237
+ if selector =~ /^([a-z][a-z0-9]*)?([#.][\w\-#.]*)?$/i
238
+ parts[:element] = ::Regexp.last_match(1)
239
+ selector_part = ::Regexp.last_match(2)
240
+
241
+ if selector_part
242
+ # Extract ID
243
+ parts[:id] = ::Regexp.last_match(1) if selector_part =~ /#([\w\-]+)/
244
+
245
+ # Extract classes
246
+ classes = selector_part.scan(/\.([\w\-]+)/).flatten
247
+ parts[:classes] = classes unless classes.empty?
248
+ end
249
+ elsif selector.start_with?('#')
250
+ # Just an ID
251
+ parts[:id] = selector[1..-1]
252
+ elsif selector.start_with?('.')
253
+ # Just a class
254
+ parts[:classes] = [selector[1..-1]]
255
+ else
256
+ # Just an element
257
+ parts[:element] = selector
258
+ end
259
+
260
+ parts
261
+ end
262
+
263
+ def matches_selector?(item, selector_parts)
264
+ line = item[:stripped]
265
+ element_info = element_selector(line)
266
+
267
+ return false unless element_info
268
+
269
+ # Check element match
270
+ return false if selector_parts[:element] && !(element_info[:element] == selector_parts[:element])
271
+
272
+ # Check ID match
273
+ return false if selector_parts[:id] && !element_info[:selector].include?("##{selector_parts[:id]}")
274
+
275
+ # Check class matches
276
+ if selector_parts[:classes]
277
+ selector_parts[:classes].each do |cls|
278
+ return false unless element_info[:selector].include?(".#{cls}")
279
+ end
280
+ end
281
+
282
+ true
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,145 @@
1
+ require_relative 'slim_manipulator'
2
+
3
+ module Blueprint
4
+ module Html2Slim
5
+ class SlimFixer < SlimManipulator
6
+ def fix_file(file_path)
7
+ content = read_file(file_path)
8
+ original_content = content.dup
9
+
10
+ fixes_applied = []
11
+
12
+ if options[:fix_slashes] != false
13
+ content, slash_fixes = fix_slash_prefix(content)
14
+ fixes_applied.concat(slash_fixes)
15
+ end
16
+
17
+ if options[:fix_multiline] != false
18
+ content, multiline_fixes = fix_multiline_text(content)
19
+ fixes_applied.concat(multiline_fixes)
20
+ end
21
+
22
+ if options[:dry_run]
23
+ if fixes_applied.any?
24
+ puts "\nChanges that would be made to #{file_path}:"
25
+ puts " Fixes: #{fixes_applied.join(", ")}"
26
+ show_diff(original_content, content) if options[:verbose]
27
+ else
28
+ puts "No issues found in #{file_path}"
29
+ end
30
+ elsif content != original_content
31
+ write_file(file_path, content)
32
+ end
33
+
34
+ { success: true, fixes: fixes_applied }
35
+ rescue StandardError => e
36
+ { success: false, error: e.message }
37
+ end
38
+
39
+ private
40
+
41
+ def fix_slash_prefix(content)
42
+ fixes = []
43
+ lines = content.split("\n")
44
+
45
+ lines.map!.with_index do |line, index|
46
+ stripped = line.strip
47
+ indent = line[/\A */]
48
+
49
+ # Fix text that starts with / after an element
50
+ if stripped =~ %r{^([a-z#.][^\s/]*)\s+(/[^/].*)$}i
51
+ element_part = ::Regexp.last_match(1)
52
+ text_part = ::Regexp.last_match(2)
53
+
54
+ # Convert to pipe notation
55
+ new_lines = [
56
+ "#{indent}#{element_part}",
57
+ "#{indent}#{" " * @indent_size}| #{text_part}"
58
+ ]
59
+
60
+ fixes << "slash text at line #{index + 1}"
61
+ new_lines
62
+ # Fix standalone text starting with slash (not a comment)
63
+ elsif stripped.start_with?('/') && !stripped.start_with?('/!') && !stripped.match?(%r{^/\s})
64
+ fixes << "slash text at line #{index + 1}"
65
+ "#{indent}| #{stripped}"
66
+ else
67
+ line
68
+ end
69
+ end
70
+
71
+ lines.flatten!
72
+ new_content = lines.join("\n")
73
+
74
+ [new_content, fixes]
75
+ end
76
+
77
+ def fix_multiline_text(content)
78
+ fixes = []
79
+ lines = content.split("\n")
80
+ result_lines = []
81
+ i = 0
82
+
83
+ while i < lines.size
84
+ line = lines[i]
85
+ stripped = line.strip
86
+ indent = line[/\A */]
87
+
88
+ # Check if this is an element with multiline text content
89
+ if stripped =~ /^([a-z#.][^\s]*)\s+(.+)$/i && i + 1 < lines.size
90
+ element_part = ::Regexp.last_match(1)
91
+ first_text = ::Regexp.last_match(2)
92
+
93
+ # Look ahead to see if next lines are continuation text
94
+ next_line_indent = lines[i + 1][/\A */]
95
+
96
+ if next_line_indent.size > indent.size && !lines[i + 1].strip.empty?
97
+ # This looks like multiline text that should use pipe notation
98
+ text_lines = [first_text]
99
+ j = i + 1
100
+
101
+ while j < lines.size
102
+ next_indent = lines[j][/\A */]
103
+ next_stripped = lines[j].strip
104
+
105
+ # Stop if we hit a line with same or less indentation
106
+ break if next_indent.size <= indent.size
107
+ # Stop if we hit Slim syntax
108
+ break if next_stripped =~ %r{^[=\-|/!#.]} || next_stripped =~ /^[a-z]+[#.\[]/i
109
+
110
+ text_lines << next_stripped
111
+ j += 1
112
+ end
113
+
114
+ if text_lines.size > 1
115
+ # Convert to proper multiline with pipes
116
+ result_lines << "#{indent}#{element_part}"
117
+ text_lines.each do |text|
118
+ result_lines << "#{indent}#{" " * @indent_size}| #{text}"
119
+ end
120
+
121
+ fixes << "multiline text at line #{i + 1}"
122
+ i = j
123
+ next
124
+ end
125
+ end
126
+ end
127
+
128
+ result_lines << line
129
+ i += 1
130
+ end
131
+
132
+ new_content = result_lines.join("\n")
133
+ [new_content, fixes]
134
+ end
135
+
136
+ def show_diff(original, modified)
137
+ puts "\n--- Original ---"
138
+ puts original
139
+ puts "\n+++ Modified +++"
140
+ puts modified
141
+ puts
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,117 @@
1
+ module Blueprint
2
+ module Html2Slim
3
+ class SlimManipulator
4
+ attr_reader :options
5
+
6
+ def initialize(options = {})
7
+ @options = options
8
+ @indent_size = options[:indent_size] || 2
9
+ end
10
+
11
+ protected
12
+
13
+ def read_file(file_path)
14
+ File.read(file_path, encoding: 'UTF-8')
15
+ end
16
+
17
+ def write_file(file_path, content)
18
+ return if options[:dry_run]
19
+
20
+ # Create backup if requested
21
+ if options[:backup]
22
+ backup_path = "#{file_path}.bak"
23
+ File.write(backup_path, read_file(file_path), encoding: 'UTF-8')
24
+ end
25
+
26
+ # Ensure content ends with newline
27
+ content += "\n" unless content.end_with?("\n")
28
+ File.write(file_path, content, encoding: 'UTF-8')
29
+ end
30
+
31
+ def parse_slim_structure(content)
32
+ lines = content.split("\n")
33
+ structure = []
34
+
35
+ lines.each_with_index do |line, index|
36
+ indent_level = line[/\A */].size / @indent_size
37
+ stripped = line.strip
38
+
39
+ next if stripped.empty?
40
+
41
+ structure << {
42
+ line: line,
43
+ stripped: stripped,
44
+ indent_level: indent_level,
45
+ line_number: index + 1,
46
+ type: detect_line_type(stripped)
47
+ }
48
+ end
49
+
50
+ structure
51
+ end
52
+
53
+ def detect_line_type(line)
54
+ case line
55
+ when /^doctype/i
56
+ :doctype
57
+ when %r{^/!}
58
+ :html_comment
59
+ when %r{^/\s}
60
+ :slim_comment
61
+ when /^-/
62
+ :ruby_code
63
+ when /^=/
64
+ :ruby_output
65
+ when /^ruby:/
66
+ :ruby_block
67
+ when /^\|/
68
+ :text_pipe
69
+ when /^[#.]/
70
+ :div_shorthand
71
+ when /^[a-z][a-z0-9]*/i
72
+ :element
73
+ else
74
+ :text
75
+ end
76
+ end
77
+
78
+ def indent_string(level)
79
+ ' ' * (@indent_size * level)
80
+ end
81
+
82
+ def rebuild_slim(structure)
83
+ structure.map do |item|
84
+ item[:modified_line] || item[:line]
85
+ end.join("\n")
86
+ end
87
+
88
+ def element_selector(line)
89
+ # Extract element, id, and classes from a Slim line
90
+ match = line.match(/^([a-z][a-z0-9]*)?([#.][\w\-#.]*)?/i)
91
+ return nil unless match
92
+
93
+ {
94
+ element: match[1] || 'div',
95
+ selector: match[2] || '',
96
+ full: match[0]
97
+ }
98
+ end
99
+
100
+ def has_slash_prefix_text?(line)
101
+ # Check if line has text that starts with forward slash
102
+ # This is a common issue that needs fixing
103
+ stripped = line.strip
104
+
105
+ # Check for inline text after element
106
+ if stripped =~ /^[a-z#.]/i
107
+ # Extract the text part after element definition
108
+ text_part = stripped.sub(/^[a-z][a-z0-9]*([#.][\w\-#.]*)?(\[.*?\])?/i, '').strip
109
+ return text_part.start_with?('/')
110
+ end
111
+
112
+ # Check for standalone text that starts with slash
113
+ stripped.start_with?('/') && !stripped.start_with?('/!') && !stripped.match?(%r{^/\s})
114
+ end
115
+ end
116
+ end
117
+ end