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.
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