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
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)
|
@@ -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
|