blueprint-html2slim 1.1.0 → 1.3.1
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
- data/CHANGELOG.md +91 -5
- data/README.md +360 -35
- data/bin/slimtool +238 -0
- data/lib/blueprint/html2slim/link_extractor.rb +203 -0
- data/lib/blueprint/html2slim/slim_extractor.rb +429 -0
- data/lib/blueprint/html2slim/slim_fixer.rb +145 -0
- data/lib/blueprint/html2slim/slim_manipulator.rb +117 -0
- data/lib/blueprint/html2slim/slim_railsifier.rb +254 -0
- data/lib/blueprint/html2slim/slim_validator.rb +170 -0
- data/lib/blueprint/html2slim/version.rb +1 -1
- metadata +9 -1
@@ -0,0 +1,429 @@
|
|
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 html head nav header footer script body]
|
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
|
+
# Clean up orphaned comments
|
36
|
+
extracted = clean_orphaned_comments(extracted)
|
37
|
+
|
38
|
+
# Rebuild the Slim content
|
39
|
+
new_content = rebuild_extracted_content(extracted)
|
40
|
+
|
41
|
+
# Write to output file
|
42
|
+
output_path = options[:output] || file_path.sub(/\.slim$/, '_extracted.slim')
|
43
|
+
write_file(output_path, new_content)
|
44
|
+
|
45
|
+
# Build appropriate response based on extraction mode
|
46
|
+
if options[:outline]
|
47
|
+
{
|
48
|
+
success: true,
|
49
|
+
mode: 'outline',
|
50
|
+
depth: options[:outline]
|
51
|
+
}
|
52
|
+
elsif options[:selector]
|
53
|
+
{
|
54
|
+
success: true,
|
55
|
+
mode: 'selector',
|
56
|
+
selector: options[:selector]
|
57
|
+
}
|
58
|
+
else
|
59
|
+
{
|
60
|
+
success: true,
|
61
|
+
removed: sections_to_remove,
|
62
|
+
kept: sections_to_keep.empty? ? nil : sections_to_keep
|
63
|
+
}
|
64
|
+
end
|
65
|
+
rescue StandardError => e
|
66
|
+
{ success: false, error: e.message }
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def normalize_selectors(selectors)
|
72
|
+
return [] unless selectors
|
73
|
+
|
74
|
+
selectors.flat_map do |selector|
|
75
|
+
selector.split(',').map(&:strip).map(&:downcase)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def extract_content(structure, keep_selectors, remove_selectors)
|
80
|
+
result = []
|
81
|
+
skip_until_indent = nil
|
82
|
+
keep_until_indent = nil
|
83
|
+
|
84
|
+
structure.each_with_index do |item, _index|
|
85
|
+
# If we're in skip mode, check if we've exited the skipped section
|
86
|
+
if skip_until_indent
|
87
|
+
next if item[:indent_level] > skip_until_indent
|
88
|
+
|
89
|
+
skip_until_indent = nil
|
90
|
+
# Continue processing this line
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
# If we're in keep mode, track when we exit
|
95
|
+
keep_until_indent = nil if keep_until_indent && item[:indent_level] <= keep_until_indent
|
96
|
+
|
97
|
+
# Determine what to do with this line
|
98
|
+
if !keep_selectors.empty?
|
99
|
+
# We have keep selectors - only keep matching sections
|
100
|
+
if should_keep_line?(item, keep_selectors)
|
101
|
+
# This line matches a keep selector
|
102
|
+
keep_until_indent = item[:indent_level]
|
103
|
+
result << item
|
104
|
+
elsif keep_until_indent && item[:indent_level] > keep_until_indent
|
105
|
+
# We're inside a kept section
|
106
|
+
result << item
|
107
|
+
end
|
108
|
+
# Otherwise, skip this line
|
109
|
+
elsif should_remove_line?(item, remove_selectors)
|
110
|
+
# No keep selectors - use remove logic
|
111
|
+
skip_until_indent = item[:indent_level]
|
112
|
+
next
|
113
|
+
# Skip this line and all its children
|
114
|
+
else
|
115
|
+
result << item
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
result
|
120
|
+
end
|
121
|
+
|
122
|
+
def should_remove_line?(item, selectors)
|
123
|
+
return false if selectors.empty?
|
124
|
+
|
125
|
+
line = item[:stripped]
|
126
|
+
|
127
|
+
selectors.any? do |selector|
|
128
|
+
case selector
|
129
|
+
when 'doctype'
|
130
|
+
item[:type] == :doctype
|
131
|
+
when 'script', 'style', 'link', 'meta'
|
132
|
+
line.start_with?(selector)
|
133
|
+
else
|
134
|
+
# Check for element match or class/id match
|
135
|
+
element_info = element_selector(line)
|
136
|
+
if element_info
|
137
|
+
element_info[:element] == selector ||
|
138
|
+
element_info[:selector].include?(".#{selector}") ||
|
139
|
+
element_info[:selector].include?("##{selector}")
|
140
|
+
else
|
141
|
+
false
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def should_keep_line?(item, selectors)
|
148
|
+
return true if selectors.empty?
|
149
|
+
|
150
|
+
line = item[:stripped]
|
151
|
+
|
152
|
+
selectors.any? do |selector|
|
153
|
+
element_info = element_selector(line)
|
154
|
+
if element_info
|
155
|
+
element_info[:element] == selector ||
|
156
|
+
element_info[:selector].include?(".#{selector}") ||
|
157
|
+
element_info[:selector].include?("##{selector}")
|
158
|
+
else
|
159
|
+
false
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def remove_outer_wrapper(structure)
|
165
|
+
return structure if structure.empty?
|
166
|
+
|
167
|
+
# Find the minimum indentation level
|
168
|
+
min_indent = structure.map { |item| item[:indent_level] }.min
|
169
|
+
|
170
|
+
# If there's only one element at the minimum level, remove it
|
171
|
+
root_elements = structure.select { |item| item[:indent_level] == min_indent }
|
172
|
+
|
173
|
+
if root_elements.size == 1 && root_elements.first[:type] == :element
|
174
|
+
# Remove the wrapper and decrease indentation of all children
|
175
|
+
structure = structure[1..-1].map do |item|
|
176
|
+
item[:indent_level] -= 1 if item[:indent_level] > min_indent
|
177
|
+
item
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
structure
|
182
|
+
end
|
183
|
+
|
184
|
+
def rebuild_extracted_content(structure)
|
185
|
+
return '' if structure.empty?
|
186
|
+
|
187
|
+
# Normalize indentation - find minimum and adjust
|
188
|
+
min_indent = structure.map { |item| item[:indent_level] }.min || 0
|
189
|
+
|
190
|
+
structure.map do |item|
|
191
|
+
adjusted_indent = item[:indent_level] - min_indent
|
192
|
+
indent_string(adjusted_indent) + item[:stripped]
|
193
|
+
end.join("\n")
|
194
|
+
end
|
195
|
+
|
196
|
+
def extract_outline(structure, max_depth)
|
197
|
+
result = []
|
198
|
+
|
199
|
+
structure.each do |item|
|
200
|
+
# Include items up to the specified depth
|
201
|
+
result << item if item[:indent_level] < max_depth
|
202
|
+
end
|
203
|
+
|
204
|
+
result
|
205
|
+
end
|
206
|
+
|
207
|
+
def extract_by_selector(structure, selector)
|
208
|
+
result = []
|
209
|
+
@current_structure = structure # Store for parent lookup
|
210
|
+
|
211
|
+
# Parse the CSS selector
|
212
|
+
selector_parts = parse_css_selector(selector)
|
213
|
+
|
214
|
+
# For child selectors like "body > section", find all matching sections
|
215
|
+
if selector_parts[:combinator] == :child
|
216
|
+
structure.each do |item|
|
217
|
+
if matches_selector?(item, selector_parts)
|
218
|
+
# Add this item and all its children
|
219
|
+
result << item
|
220
|
+
# Add children until we hit the same or lower indent level
|
221
|
+
item_index = structure.index(item)
|
222
|
+
next unless item_index
|
223
|
+
|
224
|
+
(item_index + 1...structure.size).each do |i|
|
225
|
+
child_item = structure[i]
|
226
|
+
break if child_item[:indent_level] <= item[:indent_level]
|
227
|
+
result << child_item
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
else
|
232
|
+
# Original single-section logic for simple selectors
|
233
|
+
in_selected_section = false
|
234
|
+
selected_indent = nil
|
235
|
+
|
236
|
+
structure.each do |item|
|
237
|
+
# Check if we're exiting a selected section
|
238
|
+
if in_selected_section && selected_indent && item[:indent_level] <= selected_indent
|
239
|
+
in_selected_section = false
|
240
|
+
selected_indent = nil
|
241
|
+
end
|
242
|
+
|
243
|
+
# Check if this item matches the selector
|
244
|
+
if !in_selected_section && matches_selector?(item, selector_parts)
|
245
|
+
in_selected_section = true
|
246
|
+
selected_indent = item[:indent_level]
|
247
|
+
result << item
|
248
|
+
elsif in_selected_section
|
249
|
+
result << item
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
result
|
255
|
+
end
|
256
|
+
|
257
|
+
def parse_css_selector(selector)
|
258
|
+
# Support CSS selectors: element, #id, .class, element.class, element#id
|
259
|
+
# Also support child combinator: parent > child
|
260
|
+
parts = {}
|
261
|
+
|
262
|
+
# Handle child combinator (e.g., "body > section")
|
263
|
+
if selector.include?(' > ')
|
264
|
+
parent_child = selector.split(' > ').map(&:strip)
|
265
|
+
if parent_child.size == 2
|
266
|
+
parts[:parent] = parse_simple_selector(parent_child[0])
|
267
|
+
parts[:child] = parse_simple_selector(parent_child[1])
|
268
|
+
parts[:combinator] = :child
|
269
|
+
return parts
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Handle simple selectors
|
274
|
+
parts.merge!(parse_simple_selector(selector))
|
275
|
+
parts
|
276
|
+
end
|
277
|
+
|
278
|
+
def parse_simple_selector(selector)
|
279
|
+
parts = {}
|
280
|
+
|
281
|
+
# Handle complex selectors like div.container#main
|
282
|
+
if selector =~ /^([a-z][a-z0-9]*)?([#.][\w\-#.]*)?$/i
|
283
|
+
parts[:element] = ::Regexp.last_match(1)
|
284
|
+
selector_part = ::Regexp.last_match(2)
|
285
|
+
|
286
|
+
if selector_part
|
287
|
+
# Extract ID
|
288
|
+
parts[:id] = ::Regexp.last_match(1) if selector_part =~ /#([\w\-]+)/
|
289
|
+
|
290
|
+
# Extract classes
|
291
|
+
classes = selector_part.scan(/\.([\w\-]+)/).flatten
|
292
|
+
parts[:classes] = classes unless classes.empty?
|
293
|
+
end
|
294
|
+
elsif selector.start_with?('#')
|
295
|
+
# Just an ID
|
296
|
+
parts[:id] = selector[1..-1]
|
297
|
+
elsif selector.start_with?('.')
|
298
|
+
# Just a class
|
299
|
+
parts[:classes] = [selector[1..-1]]
|
300
|
+
else
|
301
|
+
# Just an element
|
302
|
+
parts[:element] = selector
|
303
|
+
end
|
304
|
+
|
305
|
+
parts
|
306
|
+
end
|
307
|
+
|
308
|
+
def matches_selector?(item, selector_parts)
|
309
|
+
# Handle child combinator selectors
|
310
|
+
if selector_parts[:combinator] == :child
|
311
|
+
return matches_child_selector?(item, selector_parts)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Handle simple selectors
|
315
|
+
line = item[:stripped]
|
316
|
+
element_info = element_selector(line)
|
317
|
+
|
318
|
+
return false unless element_info
|
319
|
+
|
320
|
+
# Check element match
|
321
|
+
return false if selector_parts[:element] && !(element_info[:element] == selector_parts[:element])
|
322
|
+
|
323
|
+
# Check ID match
|
324
|
+
return false if selector_parts[:id] && !element_info[:selector].include?("##{selector_parts[:id]}")
|
325
|
+
|
326
|
+
# Check class matches
|
327
|
+
if selector_parts[:classes]
|
328
|
+
selector_parts[:classes].each do |cls|
|
329
|
+
return false unless element_info[:selector].include?(".#{cls}")
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
true
|
334
|
+
end
|
335
|
+
|
336
|
+
def matches_child_selector?(item, selector_parts)
|
337
|
+
# For child selector, we need to check if this item matches the child
|
338
|
+
# and verify its parent matches the parent selector
|
339
|
+
|
340
|
+
# First check if this item matches the child selector
|
341
|
+
return false unless matches_simple_selector?(item, selector_parts[:child])
|
342
|
+
|
343
|
+
# Then find its parent and check if it matches the parent selector
|
344
|
+
parent_item = find_parent_item(item)
|
345
|
+
return false unless parent_item
|
346
|
+
|
347
|
+
matches_simple_selector?(parent_item, selector_parts[:parent])
|
348
|
+
end
|
349
|
+
|
350
|
+
def matches_simple_selector?(item, selector_parts)
|
351
|
+
line = item[:stripped]
|
352
|
+
element_info = element_selector(line)
|
353
|
+
|
354
|
+
return false unless element_info
|
355
|
+
|
356
|
+
# Check element match
|
357
|
+
return false if selector_parts[:element] && !(element_info[:element] == selector_parts[:element])
|
358
|
+
|
359
|
+
# Check ID match
|
360
|
+
return false if selector_parts[:id] && !element_info[:selector].include?("##{selector_parts[:id]}")
|
361
|
+
|
362
|
+
# Check class matches
|
363
|
+
if selector_parts[:classes]
|
364
|
+
selector_parts[:classes].each do |cls|
|
365
|
+
return false unless element_info[:selector].include?(".#{cls}")
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
true
|
370
|
+
end
|
371
|
+
|
372
|
+
def find_parent_item(target_item)
|
373
|
+
# Find the parent of the target item by looking for the previous item
|
374
|
+
# with lower indentation level
|
375
|
+
target_indent = target_item[:indent_level]
|
376
|
+
target_line_num = target_item[:line_number]
|
377
|
+
|
378
|
+
# Search backwards from target item to find parent
|
379
|
+
return nil unless @current_structure
|
380
|
+
|
381
|
+
@current_structure.reverse.each do |item|
|
382
|
+
next if item[:line_number] >= target_line_num
|
383
|
+
|
384
|
+
if item[:indent_level] < target_indent
|
385
|
+
return item
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
nil
|
390
|
+
end
|
391
|
+
|
392
|
+
def clean_orphaned_comments(structure)
|
393
|
+
result = []
|
394
|
+
|
395
|
+
structure.each_with_index do |item, index|
|
396
|
+
# If this is a comment, check if the next non-comment item exists
|
397
|
+
if item[:type] == :html_comment
|
398
|
+
# Look ahead to see if there's meaningful content after this comment
|
399
|
+
has_following_content = false
|
400
|
+
|
401
|
+
(index + 1...structure.size).each do |next_index|
|
402
|
+
next_item = structure[next_index]
|
403
|
+
|
404
|
+
# If we find content at the same or lower indent level, keep the comment
|
405
|
+
if next_item[:indent_level] <= item[:indent_level] &&
|
406
|
+
next_item[:type] != :html_comment
|
407
|
+
has_following_content = true
|
408
|
+
break
|
409
|
+
end
|
410
|
+
|
411
|
+
# If we find indented content, keep the comment
|
412
|
+
if next_item[:indent_level] > item[:indent_level]
|
413
|
+
has_following_content = true
|
414
|
+
break
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Only keep the comment if there's following content
|
419
|
+
result << item if has_following_content
|
420
|
+
else
|
421
|
+
result << item
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
result
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
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
|