blueprint-html2slim 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +87 -5
- data/README.md +362 -35
- data/bin/html2slim +10 -13
- data/bin/slimtool +238 -0
- data/lib/blueprint/html2slim/converter.rb +88 -48
- data/lib/blueprint/html2slim/link_extractor.rb +203 -0
- data/lib/blueprint/html2slim/slim_extractor.rb +286 -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
data/bin/slimtool
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'thor'
|
3
|
+
require 'pathname'
|
4
|
+
require_relative '../lib/blueprint/html2slim'
|
5
|
+
|
6
|
+
class SlimToolCLI < Thor
|
7
|
+
def self.exit_on_failure?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
class_option :help, aliases: '-h', type: :boolean, desc: 'Show help'
|
12
|
+
|
13
|
+
desc 'fix FILE [FILE2 ...]', 'Fix common Slim syntax issues'
|
14
|
+
option :fix_slashes, type: :boolean, default: true, desc: 'Fix forward slashes that should be text (e.g., span /month)'
|
15
|
+
option :fix_multiline, type: :boolean, default: true, desc: 'Fix multiline text blocks using pipe notation'
|
16
|
+
option :backup, aliases: '-b', type: :boolean, desc: 'Create .bak backup before fixing'
|
17
|
+
option :dry_run, aliases: '-n', type: :boolean, desc: 'Preview changes without modifying files'
|
18
|
+
def fix(*files)
|
19
|
+
if files.empty?
|
20
|
+
puts 'Error: No files specified'
|
21
|
+
puts 'Usage: slimtool fix FILE [FILE2 ...]'
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
|
25
|
+
require_relative '../lib/blueprint/html2slim/slim_fixer'
|
26
|
+
fixer = Blueprint::Html2Slim::SlimFixer.new(options)
|
27
|
+
|
28
|
+
files.each do |file|
|
29
|
+
if File.exist?(file)
|
30
|
+
result = fixer.fix_file(file)
|
31
|
+
if result[:success]
|
32
|
+
puts "Fixed: #{file}"
|
33
|
+
puts " Issues fixed: #{result[:fixes].join(", ")}" if result[:fixes].any?
|
34
|
+
else
|
35
|
+
puts "Error fixing #{file}: #{result[:error]}"
|
36
|
+
end
|
37
|
+
else
|
38
|
+
puts "File not found: #{file}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'extract FILE', 'Extract content sections from Slim file'
|
44
|
+
option :keep, type: :array, desc: 'Keep only specified sections (e.g., main, article, .content)'
|
45
|
+
option :remove, type: :array, desc: 'Remove specified sections (e.g., head, nav, footer, script)'
|
46
|
+
option :output, aliases: '-o', desc: 'Output file path (default: FILE_extracted.slim)'
|
47
|
+
option :remove_wrapper, type: :boolean, desc: 'Remove single outer wrapper element if present'
|
48
|
+
option :outline, type: :numeric, desc: 'Extract outline up to depth N (e.g., 2 = top two levels only)'
|
49
|
+
option :selector, type: :string, desc: 'CSS selector for fragment extraction (#id, .class, element)'
|
50
|
+
def extract(file)
|
51
|
+
unless File.exist?(file)
|
52
|
+
puts "Error: File not found: #{file}"
|
53
|
+
exit 1
|
54
|
+
end
|
55
|
+
|
56
|
+
require_relative '../lib/blueprint/html2slim/slim_extractor'
|
57
|
+
extractor = Blueprint::Html2Slim::SlimExtractor.new(options)
|
58
|
+
|
59
|
+
result = extractor.extract_file(file)
|
60
|
+
if result[:success]
|
61
|
+
output_path = options[:output] || file.sub(/\.slim$/, '_extracted.slim')
|
62
|
+
puts "Extracted content to: #{output_path}"
|
63
|
+
puts " Removed: #{result[:removed].join(", ")}" if result[:removed]&.any?
|
64
|
+
puts " Kept: #{result[:kept].join(", ")}" if result[:kept]&.any?
|
65
|
+
else
|
66
|
+
puts "Error extracting from #{file}: #{result[:error]}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'validate FILE [FILE2 ...]', 'Validate Slim syntax and check for issues'
|
71
|
+
option :strict, type: :boolean, desc: 'Enable strict mode (check tabs, inline styles, etc.)'
|
72
|
+
option :check_rails, type: :boolean, desc: 'Check Rails conventions (helpers, CSRF, asset pipeline)'
|
73
|
+
def validate(*files)
|
74
|
+
if files.empty?
|
75
|
+
puts 'Error: No files specified'
|
76
|
+
puts 'Usage: slimtool validate FILE [FILE2 ...]'
|
77
|
+
exit 1
|
78
|
+
end
|
79
|
+
|
80
|
+
require_relative '../lib/blueprint/html2slim/slim_validator'
|
81
|
+
validator = Blueprint::Html2Slim::SlimValidator.new(options)
|
82
|
+
|
83
|
+
total_errors = 0
|
84
|
+
total_warnings = 0
|
85
|
+
|
86
|
+
files.each do |file|
|
87
|
+
if File.exist?(file)
|
88
|
+
result = validator.validate_file(file)
|
89
|
+
if result[:valid]
|
90
|
+
puts "✓ #{file} - Valid"
|
91
|
+
else
|
92
|
+
puts "✗ #{file} - Invalid"
|
93
|
+
result[:errors]&.each { |error| puts " ERROR: #{error}" }
|
94
|
+
total_errors += result[:errors]&.size || 0
|
95
|
+
end
|
96
|
+
|
97
|
+
if result[:warnings]&.any?
|
98
|
+
result[:warnings].each { |warning| puts " WARNING: #{warning}" }
|
99
|
+
total_warnings += result[:warnings].size
|
100
|
+
end
|
101
|
+
else
|
102
|
+
puts "File not found: #{file}"
|
103
|
+
total_errors += 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
puts "\nValidation complete: #{total_errors} error(s), #{total_warnings} warning(s)"
|
108
|
+
exit(1) if total_errors.positive?
|
109
|
+
end
|
110
|
+
|
111
|
+
desc 'railsify FILE [FILE2 ...]', 'Convert static Slim to Rails conventions'
|
112
|
+
option :add_helpers, type: :boolean, default: true, desc: 'Convert links to Rails helpers (requires --mappings)'
|
113
|
+
option :use_assets, type: :boolean, default: true, desc: 'Convert CDN assets to Rails asset pipeline'
|
114
|
+
option :add_csrf, type: :boolean, desc: 'Add CSRF meta tags to head section'
|
115
|
+
option :backup, aliases: '-b', type: :boolean, desc: 'Create .bak backup before converting'
|
116
|
+
option :dry_run, aliases: '-n', type: :boolean, desc: 'Preview changes without modifying files'
|
117
|
+
option :mappings, type: :string, desc: 'JSON/YAML file with URL-to-helper mappings (required for link conversion)'
|
118
|
+
def railsify(*files)
|
119
|
+
if files.empty?
|
120
|
+
puts 'Error: No files specified'
|
121
|
+
puts 'Usage: slimtool railsify FILE [FILE2 ...]'
|
122
|
+
exit 1
|
123
|
+
end
|
124
|
+
|
125
|
+
require_relative '../lib/blueprint/html2slim/slim_railsifier'
|
126
|
+
railsifier = Blueprint::Html2Slim::SlimRailsifier.new(options)
|
127
|
+
|
128
|
+
files.each do |file|
|
129
|
+
if File.exist?(file)
|
130
|
+
result = railsifier.railsify_file(file)
|
131
|
+
if result[:success]
|
132
|
+
puts "Railsified: #{file}"
|
133
|
+
puts " Conversions: #{result[:conversions].join(", ")}" if result[:conversions]&.any?
|
134
|
+
else
|
135
|
+
puts "Error converting #{file}: #{result[:error]}"
|
136
|
+
end
|
137
|
+
else
|
138
|
+
puts "File not found: #{file}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
desc 'extract-links FILE [FILE2 ...]', 'Find and report all hardcoded links'
|
144
|
+
option :output, aliases: '-o', desc: 'Save results to file (auto-detect format by extension)'
|
145
|
+
option :format, default: 'json', enum: %w[json yaml text], desc: 'Output format (json, yaml, or text)'
|
146
|
+
def extract_links(*files)
|
147
|
+
if files.empty?
|
148
|
+
puts 'Error: No files specified'
|
149
|
+
puts 'Usage: slimtool extract-links FILE [FILE2 ...]'
|
150
|
+
exit 1
|
151
|
+
end
|
152
|
+
|
153
|
+
require_relative '../lib/blueprint/html2slim/link_extractor'
|
154
|
+
extractor = Blueprint::Html2Slim::LinkExtractor.new(options)
|
155
|
+
|
156
|
+
all_links = {}
|
157
|
+
|
158
|
+
files.each do |file|
|
159
|
+
if File.exist?(file)
|
160
|
+
result = extractor.extract_links(file)
|
161
|
+
if result[:success]
|
162
|
+
all_links[file] = result[:links] if result[:links].any?
|
163
|
+
else
|
164
|
+
puts "Error extracting from #{file}: #{result[:error]}"
|
165
|
+
end
|
166
|
+
else
|
167
|
+
puts "File not found: #{file}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
if all_links.any?
|
172
|
+
if options[:output]
|
173
|
+
extractor.save_links(all_links, options[:output])
|
174
|
+
puts "Extracted links saved to: #{options[:output]}"
|
175
|
+
else
|
176
|
+
extractor.display_links(all_links)
|
177
|
+
end
|
178
|
+
|
179
|
+
total = all_links.values.flatten.size
|
180
|
+
puts "\nTotal: #{total} hardcoded link(s) found in #{all_links.keys.size} file(s)"
|
181
|
+
else
|
182
|
+
puts 'No hardcoded links found'
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
desc 'version', 'Show version'
|
187
|
+
def version
|
188
|
+
require_relative '../lib/blueprint/html2slim/version'
|
189
|
+
puts "slimtool #{Blueprint::Html2Slim::VERSION}"
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.start(given_args = ARGV, config = {})
|
193
|
+
# Show general help for no args or 'help' without specific command
|
194
|
+
if given_args.empty? || (given_args == ['help']) || (given_args == ['-h']) || (given_args == ['--help'])
|
195
|
+
puts 'Slim Template Manipulation Tool'
|
196
|
+
puts ''
|
197
|
+
puts 'Usage: slimtool COMMAND [OPTIONS] FILE [FILE2 ...]'
|
198
|
+
puts ''
|
199
|
+
puts 'Commands:'
|
200
|
+
puts ' fix Fix common Slim syntax issues'
|
201
|
+
puts ' extract Extract content sections from Slim files'
|
202
|
+
puts ' validate Validate Slim syntax'
|
203
|
+
puts ' railsify Convert to Rails conventions'
|
204
|
+
puts ' extract-links Extract hardcoded links from templates'
|
205
|
+
puts ' version Show version'
|
206
|
+
puts ' help Show this help message'
|
207
|
+
puts ''
|
208
|
+
puts 'Examples:'
|
209
|
+
puts ' # Fix text starting with slash'
|
210
|
+
puts ' slimtool fix pricing.slim'
|
211
|
+
puts ''
|
212
|
+
puts ' # Extract main content, removing navigation'
|
213
|
+
puts ' slimtool extract index.slim --remove head,nav,footer'
|
214
|
+
puts ''
|
215
|
+
puts ' # Extract high-level structure only'
|
216
|
+
puts ' slimtool extract page.slim --outline 2'
|
217
|
+
puts ''
|
218
|
+
puts ' # Extract specific section by CSS selector'
|
219
|
+
puts ' slimtool extract page.slim --selector "#content"'
|
220
|
+
puts ''
|
221
|
+
puts ' # Validate with strict style checking'
|
222
|
+
puts ' slimtool validate *.slim --strict --check-rails'
|
223
|
+
puts ''
|
224
|
+
puts ' # Convert to Rails helpers with custom mappings'
|
225
|
+
puts ' slimtool railsify template.slim --mappings urls.json'
|
226
|
+
puts ''
|
227
|
+
puts ' # Find all hardcoded links'
|
228
|
+
puts ' slimtool extract-links views/**/*.slim -o links.json'
|
229
|
+
puts ''
|
230
|
+
puts 'Run "slimtool COMMAND --help" for more information on a command.'
|
231
|
+
exit 0
|
232
|
+
else
|
233
|
+
super
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
SlimToolCLI.start(ARGV)
|
@@ -17,25 +17,25 @@ module Blueprint
|
|
17
17
|
|
18
18
|
def convert(html_content)
|
19
19
|
lines = []
|
20
|
-
|
20
|
+
|
21
21
|
# Handle DOCTYPE declaration
|
22
22
|
if html_content =~ /<!DOCTYPE\s+(.+?)>/i
|
23
23
|
doctype_content = ::Regexp.last_match(1)
|
24
|
-
if doctype_content =~ /strict/i
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
24
|
+
lines << if doctype_content =~ /strict/i
|
25
|
+
'doctype strict'
|
26
|
+
elsif doctype_content =~ /transitional/i
|
27
|
+
'doctype transitional'
|
28
|
+
elsif doctype_content =~ /frameset/i
|
29
|
+
'doctype frameset'
|
30
|
+
elsif doctype_content =~ /html$/i
|
31
|
+
'doctype html'
|
32
|
+
else
|
33
|
+
'doctype'
|
34
|
+
end
|
35
35
|
# Remove DOCTYPE from content for further processing
|
36
36
|
html_content = html_content.sub(/<!DOCTYPE\s+.+?>/i, '')
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
html_content = preprocess_erb(html_content)
|
40
40
|
# Use HTML.parse for full documents, DocumentFragment for fragments
|
41
41
|
if html_content =~ /<html/i
|
@@ -68,7 +68,7 @@ module Blueprint
|
|
68
68
|
def preprocess_erb(content)
|
69
69
|
# Convert ERB blocks to span elements to preserve hierarchy
|
70
70
|
# This approach is inspired by the original html2slim gem
|
71
|
-
|
71
|
+
|
72
72
|
# Keep ERB tags in attributes unchanged by temporarily replacing them
|
73
73
|
erb_in_attrs = []
|
74
74
|
content = content.gsub(/(<[^>]*)(<%=?.+?%>)([^>]*>)/) do
|
@@ -79,65 +79,79 @@ module Blueprint
|
|
79
79
|
erb_in_attrs << erb
|
80
80
|
"#{before}#{placeholder}#{after}"
|
81
81
|
end
|
82
|
-
|
82
|
+
|
83
83
|
# Handle multi-line ERB blocks first (with m flag for multiline)
|
84
84
|
content = content.gsub(/<%\s*\n(.*?)\n\s*-?%>/m) do
|
85
|
-
|
86
|
-
#
|
87
|
-
|
85
|
+
code_block = ::Regexp.last_match(1).strip
|
86
|
+
# Mark as multiline block for special handling
|
87
|
+
"<!--ERB_MULTILINE_CODE:#{code_block.gsub("-->", "__ARROW__")}-->"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Handle multiline ERB output blocks (e.g., <%= form_with(...) spanning multiple lines %>)
|
91
|
+
content = content.gsub(/<%=\s*\n(.*?)\n\s*-?%>/m) do
|
92
|
+
code_block = ::Regexp.last_match(1).strip
|
93
|
+
# Check if it ends with do block
|
94
|
+
if code_block =~ /\bdo\s*(\|[^|]*\|)?\s*$/
|
95
|
+
# Convert to single line for block processing
|
96
|
+
single_line = code_block.gsub(/\s+/, ' ')
|
97
|
+
"<%= #{single_line} %>" # Keep for block processing
|
98
|
+
else
|
99
|
+
# Mark as multiline output for special handling
|
100
|
+
"<!--ERB_MULTILINE_OUTPUT:#{code_block.gsub("-->", "__ARROW__")}-->"
|
101
|
+
end
|
88
102
|
end
|
89
|
-
|
103
|
+
|
90
104
|
# Convert simple ERB output tags that don't create blocks
|
91
105
|
# This prevents them from being caught by the block regex
|
92
106
|
content = content.gsub(/<%=\s*([^%]+?)\s*%>/) do
|
93
107
|
code = ::Regexp.last_match(1).strip
|
94
108
|
# Skip if it's a do block
|
95
109
|
if code =~ /\bdo\s*(\|[^|]*\|)?\s*$/
|
96
|
-
"<%= #{code} %>"
|
110
|
+
"<%= #{code} %>" # Keep original, will be processed later
|
97
111
|
else
|
98
112
|
%(<!--ERB_OUTPUT:#{code}-->)
|
99
113
|
end
|
100
114
|
end
|
101
|
-
|
115
|
+
|
102
116
|
# Convert ERB blocks that create structure (do...end, if...end, etc.)
|
103
117
|
# to span elements so their content becomes proper children
|
104
|
-
content = content.gsub(/<%(-|=)?\s*((\s*(case|if|for|unless|until|while) .+?)|.+?do\s*(\|[^|]*\|)?\s*)-?%>/) do
|
118
|
+
content = content.gsub(/<%(-|=)?\s*((\s*(case|if|for|unless|until|while) .+?)|.+?do\s*(\|[^|]*\|)?\s*)-?%>/m) do
|
105
119
|
type = ::Regexp.last_match(1)
|
106
120
|
code = ::Regexp.last_match(2).strip
|
107
121
|
# Preserve whether it was = or - in the code attribute
|
108
122
|
prefix = type == '=' ? '=' : ''
|
109
|
-
%(<span erb-code="#{prefix}#{code.gsub('"',
|
123
|
+
%(<span erb-code="#{prefix}#{code.gsub('"', """)}">)
|
110
124
|
end
|
111
|
-
|
125
|
+
|
112
126
|
# Handle else
|
113
127
|
content = content.gsub(/<%-?\s*else\s*-?%>/, %(</span><span erb-code="else">))
|
114
|
-
|
128
|
+
|
115
129
|
# Handle elsif
|
116
130
|
content = content.gsub(/<%-?\s*(elsif .+?)\s*-?%>/) do
|
117
131
|
code = ::Regexp.last_match(1).strip
|
118
|
-
%(</span><span erb-code="#{code.gsub('"',
|
132
|
+
%(</span><span erb-code="#{code.gsub('"', """)}">)
|
119
133
|
end
|
120
|
-
|
134
|
+
|
121
135
|
# Handle when
|
122
136
|
content = content.gsub(/<%-?\s*(when .+?)\s*-?%>/) do
|
123
137
|
code = ::Regexp.last_match(1).strip
|
124
|
-
%(</span><span erb-code="#{code.gsub('"',
|
138
|
+
%(</span><span erb-code="#{code.gsub('"', """)}">)
|
125
139
|
end
|
126
|
-
|
140
|
+
|
127
141
|
# Handle end statements - close the span
|
128
142
|
content = content.gsub(/<%\s*(end|}|end\s+-)\s*%>/, %(</span>))
|
129
|
-
|
143
|
+
|
130
144
|
# Convert any remaining ERB code tags to comments
|
131
145
|
content = content.gsub(/<%-?\s*(.+?)\s*%>/) do
|
132
146
|
code = ::Regexp.last_match(1).strip
|
133
147
|
%(<!--ERB_CODE:#{code}-->)
|
134
148
|
end
|
135
|
-
|
149
|
+
|
136
150
|
# Restore ERB tags in attributes
|
137
151
|
erb_in_attrs.each_with_index do |erb, i|
|
138
152
|
content = content.gsub("ERB_IN_ATTR_#{i}", erb)
|
139
153
|
end
|
140
|
-
|
154
|
+
|
141
155
|
content
|
142
156
|
end
|
143
157
|
|
@@ -161,24 +175,24 @@ module Blueprint
|
|
161
175
|
# Check if this is an ERB span element
|
162
176
|
if node.name == 'span' && node['erb-code']
|
163
177
|
erb_code = node['erb-code'].gsub('"', '"')
|
164
|
-
|
178
|
+
|
165
179
|
# Determine if it's output (=) or code (-)
|
166
|
-
if erb_code =~ /^(if|unless|case|for|while|elsif|else|when)\b/
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
180
|
+
lines << if erb_code =~ /^(if|unless|case|for|while|elsif|else|when)\b/
|
181
|
+
"#{indent}- #{erb_code}"
|
182
|
+
elsif erb_code.start_with?('=')
|
183
|
+
# It was originally <%= ... %>, use = prefix
|
184
|
+
"#{indent}= #{erb_code[1..-1].strip}"
|
185
|
+
else
|
186
|
+
# It was originally <% ... %>, use - prefix
|
187
|
+
"#{indent}- #{erb_code}"
|
188
|
+
end
|
189
|
+
|
176
190
|
# Process children with increased depth
|
177
191
|
node.children.each do |child|
|
178
192
|
child_lines = process_node(child, depth + 1)
|
179
193
|
lines.concat(child_lines) unless child_lines.empty?
|
180
194
|
end
|
181
|
-
|
195
|
+
|
182
196
|
return lines
|
183
197
|
end
|
184
198
|
|
@@ -206,6 +220,10 @@ module Blueprint
|
|
206
220
|
text = process_inline_text(text.strip)
|
207
221
|
if text.empty?
|
208
222
|
lines << "#{indent}#{tag_line}"
|
223
|
+
elsif text.start_with?('/')
|
224
|
+
# Text starting with / needs pipe notation to avoid being treated as comment
|
225
|
+
lines << "#{indent}#{tag_line}"
|
226
|
+
lines << "#{" " * ((depth + 1) * @indent_size)}| #{text}"
|
209
227
|
else
|
210
228
|
lines << "#{indent}#{tag_line} #{text}"
|
211
229
|
end
|
@@ -227,7 +245,7 @@ module Blueprint
|
|
227
245
|
# Strip and split classes, filtering out empty strings
|
228
246
|
classes = node['class']&.strip&.split(/\s+/)&.reject(&:empty?) || []
|
229
247
|
attributes = collect_attributes(node)
|
230
|
-
|
248
|
+
|
231
249
|
# Treat empty id as no id
|
232
250
|
id = nil if id && id.strip.empty?
|
233
251
|
|
@@ -299,14 +317,36 @@ module Blueprint
|
|
299
317
|
# Extract indentation level if present
|
300
318
|
extra_indent = 0
|
301
319
|
if comment_text =~ /:INDENT:(\d+)$/
|
302
|
-
extra_indent =
|
320
|
+
extra_indent = ::Regexp.last_match(1).to_i
|
303
321
|
comment_text = comment_text.sub(/:INDENT:\d+$/, '')
|
304
322
|
end
|
305
|
-
|
323
|
+
|
306
324
|
total_depth = depth + extra_indent
|
307
325
|
indent = ' ' * (total_depth * @indent_size)
|
308
326
|
|
309
|
-
if comment_text.start_with?('
|
327
|
+
if comment_text.start_with?('ERB_MULTILINE_CODE:')
|
328
|
+
erb_content = comment_text.sub('ERB_MULTILINE_CODE:', '').gsub('__ARROW__', '-->')
|
329
|
+
# Use ruby: block for multiline code
|
330
|
+
lines = ["#{indent}ruby:"]
|
331
|
+
erb_content.lines.each do |line|
|
332
|
+
lines << "#{indent} #{line.rstrip}"
|
333
|
+
end
|
334
|
+
lines
|
335
|
+
elsif comment_text.start_with?('ERB_MULTILINE_OUTPUT:')
|
336
|
+
erb_content = comment_text.sub('ERB_MULTILINE_OUTPUT:', '').gsub('__ARROW__', '-->')
|
337
|
+
# For multiline output, use line continuation
|
338
|
+
lines = erb_content.lines.map(&:rstrip)
|
339
|
+
if lines.length == 1
|
340
|
+
["#{indent}= #{lines[0]}"]
|
341
|
+
else
|
342
|
+
result = ["#{indent}= #{lines[0]} \\"]
|
343
|
+
lines[1..-2].each do |line|
|
344
|
+
result << "#{indent} #{line} \\"
|
345
|
+
end
|
346
|
+
result << "#{indent} #{lines[-1]}" if lines.length > 1
|
347
|
+
result
|
348
|
+
end
|
349
|
+
elsif comment_text.start_with?('ERB_OUTPUT_BLOCK:')
|
310
350
|
erb_content = comment_text.sub('ERB_OUTPUT_BLOCK:', '')
|
311
351
|
["#{indent}= #{erb_content}"]
|
312
352
|
elsif comment_text.start_with?('ERB_OUTPUT:')
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require_relative 'slim_manipulator'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Blueprint
|
6
|
+
module Html2Slim
|
7
|
+
class LinkExtractor < SlimManipulator
|
8
|
+
def extract_links(file_path)
|
9
|
+
content = read_file(file_path)
|
10
|
+
structure = parse_slim_structure(content)
|
11
|
+
|
12
|
+
links = []
|
13
|
+
|
14
|
+
structure.each do |item|
|
15
|
+
line = item[:stripped]
|
16
|
+
line_num = item[:line_number]
|
17
|
+
|
18
|
+
# Extract links from anchor tags
|
19
|
+
if line =~ /^(\s*)a\[.*href=["']([^"']+)["']/
|
20
|
+
href = ::Regexp.last_match(2)
|
21
|
+
# Identify hardcoded links (not Rails helpers, not variables)
|
22
|
+
if is_hardcoded_link?(href)
|
23
|
+
links << {
|
24
|
+
line: line_num,
|
25
|
+
type: 'anchor',
|
26
|
+
href: href,
|
27
|
+
full_line: line,
|
28
|
+
suggested_helper: suggest_rails_helper(href)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Extract links from form actions
|
34
|
+
if line =~ /^(\s*)form\[.*action=["']([^"']+)["']/
|
35
|
+
action = ::Regexp.last_match(2)
|
36
|
+
if is_hardcoded_link?(action)
|
37
|
+
links << {
|
38
|
+
line: line_num,
|
39
|
+
type: 'form',
|
40
|
+
href: action,
|
41
|
+
full_line: line,
|
42
|
+
suggested_helper: suggest_rails_helper(action)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Extract stylesheet links
|
48
|
+
if line =~ /link\[.*href=["']([^"']+\.css[^"']*)["']/
|
49
|
+
href = ::Regexp.last_match(1)
|
50
|
+
if !href.start_with?('http') && !href.start_with?('//')
|
51
|
+
links << {
|
52
|
+
line: line_num,
|
53
|
+
type: 'stylesheet',
|
54
|
+
href: href,
|
55
|
+
full_line: line,
|
56
|
+
suggested_helper: "stylesheet_link_tag '#{File.basename(href, ".*")}'"
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Extract script sources
|
62
|
+
if line =~ /script\[.*src=["']([^"']+\.js[^"']*)["']/
|
63
|
+
src = ::Regexp.last_match(1)
|
64
|
+
if !src.start_with?('http') && !src.start_with?('//')
|
65
|
+
links << {
|
66
|
+
line: line_num,
|
67
|
+
type: 'javascript',
|
68
|
+
href: src,
|
69
|
+
full_line: line,
|
70
|
+
suggested_helper: "javascript_include_tag '#{File.basename(src, ".*")}'"
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Extract image sources
|
76
|
+
next unless line =~ /img\[.*src=["']([^"']+)["']/
|
77
|
+
|
78
|
+
src = ::Regexp.last_match(1)
|
79
|
+
next unless !src.start_with?('http') && !src.start_with?('//') && !src.include?('<%')
|
80
|
+
|
81
|
+
links << {
|
82
|
+
line: line_num,
|
83
|
+
type: 'image',
|
84
|
+
href: src,
|
85
|
+
full_line: line,
|
86
|
+
suggested_helper: "image_tag '#{src}'"
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
{ success: true, links: links }
|
91
|
+
rescue StandardError => e
|
92
|
+
{ success: false, error: e.message }
|
93
|
+
end
|
94
|
+
|
95
|
+
def save_links(all_links, output_path)
|
96
|
+
format = options[:format] || 'json'
|
97
|
+
|
98
|
+
case format
|
99
|
+
when 'json'
|
100
|
+
File.write(output_path, JSON.pretty_generate(all_links))
|
101
|
+
when 'yaml'
|
102
|
+
File.write(output_path, all_links.to_yaml)
|
103
|
+
when 'text'
|
104
|
+
content = []
|
105
|
+
all_links.each do |file, links|
|
106
|
+
content << "File: #{file}"
|
107
|
+
links.each do |link|
|
108
|
+
content << " Line #{link[:line]} (#{link[:type]}): #{link[:href]}"
|
109
|
+
content << " Suggested: #{link[:suggested_helper]}" if link[:suggested_helper]
|
110
|
+
end
|
111
|
+
content << ''
|
112
|
+
end
|
113
|
+
File.write(output_path, content.join("\n"))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def display_links(all_links)
|
118
|
+
all_links.each do |file, links|
|
119
|
+
puts "\n#{file}:"
|
120
|
+
links.each do |link|
|
121
|
+
puts " Line #{link[:line]} (#{link[:type]}): #{link[:href]}"
|
122
|
+
puts " → Suggested: #{link[:suggested_helper]}" if link[:suggested_helper]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def is_hardcoded_link?(href)
|
130
|
+
# It's hardcoded if it's not a Rails helper or ERB expression
|
131
|
+
return false if href.include?('_path') || href.include?('_url')
|
132
|
+
return false if href.include?('<%') || href.include?('#{')
|
133
|
+
return false if href.start_with?('@') || href.start_with?(':')
|
134
|
+
|
135
|
+
# It's hardcoded if it's a static file or path
|
136
|
+
href.match?(/\.(html|htm|php|jsp|asp)$/) ||
|
137
|
+
href.match?(%r{^/\w+}) ||
|
138
|
+
href == '#' ||
|
139
|
+
href.match?(/^[a-z]+\.html$/i)
|
140
|
+
end
|
141
|
+
|
142
|
+
def suggest_rails_helper(href)
|
143
|
+
# Exact matches for common pages
|
144
|
+
common_mappings = {
|
145
|
+
'index.html' => 'root_path',
|
146
|
+
'home.html' => 'root_path',
|
147
|
+
'login.html' => 'login_path',
|
148
|
+
'signin.html' => 'login_path',
|
149
|
+
'signup.html' => 'signup_path',
|
150
|
+
'register.html' => 'new_user_registration_path',
|
151
|
+
'about.html' => 'about_path',
|
152
|
+
'contact.html' => 'contact_path',
|
153
|
+
'privacy.html' => 'privacy_path',
|
154
|
+
'terms.html' => 'terms_path',
|
155
|
+
'dashboard.html' => 'dashboard_path',
|
156
|
+
'profile.html' => 'profile_path',
|
157
|
+
'settings.html' => 'settings_path'
|
158
|
+
}
|
159
|
+
|
160
|
+
# Check for exact match
|
161
|
+
clean_href = href.sub(%r{^/}, '')
|
162
|
+
return common_mappings[clean_href] if common_mappings[clean_href]
|
163
|
+
|
164
|
+
# Handle resource patterns
|
165
|
+
case href
|
166
|
+
when %r{^/?users?/(\d+|:id)}
|
167
|
+
'user_path(@user)'
|
168
|
+
when %r{^/?users?$}, '/users'
|
169
|
+
'users_path'
|
170
|
+
when %r{^/?posts?/(\d+|:id)}
|
171
|
+
'post_path(@post)'
|
172
|
+
when %r{^/?posts?$}, '/posts'
|
173
|
+
'posts_path'
|
174
|
+
when %r{^/?articles?/(\d+|:id)}
|
175
|
+
'article_path(@article)'
|
176
|
+
when %r{^/?articles?$}, '/articles'
|
177
|
+
'articles_path'
|
178
|
+
when %r{^/?products?/(\d+|:id)}
|
179
|
+
'product_path(@product)'
|
180
|
+
when %r{^/?products?$}, '/products'
|
181
|
+
'products_path'
|
182
|
+
when /^#/
|
183
|
+
nil # Anchor links don't need conversion
|
184
|
+
else
|
185
|
+
# Generic suggestion based on path
|
186
|
+
path = href.sub(%r{^/}, '').sub(/\.\w+$/, '')
|
187
|
+
return nil if path.empty?
|
188
|
+
|
189
|
+
# Convert path to Rails helper format
|
190
|
+
path_parts = path.split('/')
|
191
|
+
if path_parts.last =~ /^\d+$/
|
192
|
+
# Looks like a show action
|
193
|
+
resource = path_parts[-2]
|
194
|
+
"#{resource.singularize}_path(@#{resource.singularize})"
|
195
|
+
else
|
196
|
+
# Looks like an index or named route
|
197
|
+
"#{path.gsub("/", "_").gsub("-", "_")}_path"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|