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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -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 +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
@@ -0,0 +1,254 @@
|
|
1
|
+
require_relative 'slim_manipulator'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Blueprint
|
5
|
+
module Html2Slim
|
6
|
+
class SlimRailsifier < SlimManipulator
|
7
|
+
def railsify_file(file_path)
|
8
|
+
content = read_file(file_path)
|
9
|
+
original_content = content.dup
|
10
|
+
conversions = []
|
11
|
+
|
12
|
+
# Load custom mappings if provided
|
13
|
+
@custom_mappings = load_custom_mappings if options[:mappings]
|
14
|
+
|
15
|
+
if options[:add_helpers] != false
|
16
|
+
content, link_conversions = convert_links_to_helpers(content)
|
17
|
+
conversions.concat(link_conversions)
|
18
|
+
end
|
19
|
+
|
20
|
+
if options[:use_assets] != false
|
21
|
+
content, asset_conversions = convert_assets_to_pipeline(content)
|
22
|
+
conversions.concat(asset_conversions)
|
23
|
+
end
|
24
|
+
|
25
|
+
if options[:add_csrf]
|
26
|
+
content, csrf_additions = add_csrf_protection(content)
|
27
|
+
conversions.concat(csrf_additions)
|
28
|
+
end
|
29
|
+
|
30
|
+
if options[:dry_run]
|
31
|
+
if conversions.any?
|
32
|
+
puts "\nChanges that would be made to #{file_path}:"
|
33
|
+
puts " Conversions: #{conversions.join(", ")}"
|
34
|
+
show_diff(original_content, content) if options[:verbose]
|
35
|
+
else
|
36
|
+
puts "No Rails conversions needed for #{file_path}"
|
37
|
+
end
|
38
|
+
elsif content != original_content
|
39
|
+
write_file(file_path, content)
|
40
|
+
end
|
41
|
+
|
42
|
+
{ success: true, conversions: conversions }
|
43
|
+
rescue StandardError => e
|
44
|
+
{ success: false, error: e.message }
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def load_custom_mappings
|
50
|
+
return {} unless options[:mappings] && File.exist?(options[:mappings])
|
51
|
+
|
52
|
+
begin
|
53
|
+
content = File.read(options[:mappings])
|
54
|
+
if options[:mappings].end_with?('.json')
|
55
|
+
JSON.parse(content)
|
56
|
+
elsif options[:mappings].end_with?('.yml', '.yaml')
|
57
|
+
require 'yaml'
|
58
|
+
YAML.load_file(options[:mappings])
|
59
|
+
else
|
60
|
+
# Try to parse as JSON by default
|
61
|
+
JSON.parse(content)
|
62
|
+
end
|
63
|
+
rescue StandardError => e
|
64
|
+
puts "Warning: Failed to load mappings from #{options[:mappings]}: #{e.message}"
|
65
|
+
{}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def convert_links_to_helpers(content)
|
70
|
+
conversions = []
|
71
|
+
lines = content.split("\n")
|
72
|
+
|
73
|
+
lines.map!.with_index do |line, index|
|
74
|
+
if line =~ /^(\s*)a\[href="([^"]+)"([^\]]*)\](.*)/
|
75
|
+
indent = ::Regexp.last_match(1)
|
76
|
+
href = ::Regexp.last_match(2)
|
77
|
+
attrs = ::Regexp.last_match(3)
|
78
|
+
text = ::Regexp.last_match(4).strip
|
79
|
+
|
80
|
+
# Convert known static paths to Rails helpers (only if mappings provided)
|
81
|
+
rails_path = path_to_rails_helper(href)
|
82
|
+
if rails_path
|
83
|
+
conversions << "link at line #{index + 1}"
|
84
|
+
"#{indent}= link_to \"#{text}\", #{rails_path}#{format_link_options(attrs)}"
|
85
|
+
else
|
86
|
+
line
|
87
|
+
end
|
88
|
+
else
|
89
|
+
line
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
[lines.join("\n"), conversions]
|
94
|
+
end
|
95
|
+
|
96
|
+
def convert_assets_to_pipeline(content)
|
97
|
+
conversions = []
|
98
|
+
lines = content.split("\n")
|
99
|
+
|
100
|
+
lines.map!.with_index do |line, index|
|
101
|
+
# Convert stylesheet links
|
102
|
+
if line =~ /^(\s*)link\[.*href="([^"]+\.css[^"]*)"/
|
103
|
+
indent = ::Regexp.last_match(1)
|
104
|
+
href = ::Regexp.last_match(2)
|
105
|
+
|
106
|
+
if !href.start_with?('http') && !href.start_with?('//')
|
107
|
+
asset_name = File.basename(href, '.*')
|
108
|
+
conversions << "stylesheet at line #{index + 1}"
|
109
|
+
"#{indent}= stylesheet_link_tag '#{asset_name}'"
|
110
|
+
else
|
111
|
+
line
|
112
|
+
end
|
113
|
+
# Convert script tags
|
114
|
+
elsif line =~ /^(\s*)script\[.*src="([^"]+\.js[^"]*)"/
|
115
|
+
indent = ::Regexp.last_match(1)
|
116
|
+
src = ::Regexp.last_match(2)
|
117
|
+
|
118
|
+
if !src.start_with?('http') && !src.start_with?('//')
|
119
|
+
asset_name = File.basename(src, '.*')
|
120
|
+
conversions << "javascript at line #{index + 1}"
|
121
|
+
"#{indent}= javascript_include_tag '#{asset_name}'"
|
122
|
+
else
|
123
|
+
line
|
124
|
+
end
|
125
|
+
# Convert image tags
|
126
|
+
elsif line =~ /^(\s*)img\[src="([^"]+)"([^\]]*)\]/
|
127
|
+
indent = ::Regexp.last_match(1)
|
128
|
+
src = ::Regexp.last_match(2)
|
129
|
+
attrs = ::Regexp.last_match(3)
|
130
|
+
|
131
|
+
if !src.start_with?('http') && !src.start_with?('//')
|
132
|
+
conversions << "image at line #{index + 1}"
|
133
|
+
"#{indent}= image_tag '#{src}'#{format_image_options(attrs)}"
|
134
|
+
else
|
135
|
+
line
|
136
|
+
end
|
137
|
+
else
|
138
|
+
line
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
[lines.join("\n"), conversions]
|
143
|
+
end
|
144
|
+
|
145
|
+
def add_csrf_protection(content)
|
146
|
+
conversions = []
|
147
|
+
lines = content.split("\n")
|
148
|
+
|
149
|
+
# Find head section and add CSRF meta tags
|
150
|
+
head_index = lines.index { |line| line.strip.start_with?('head') }
|
151
|
+
|
152
|
+
if head_index
|
153
|
+
indent = lines[head_index][/\A */]
|
154
|
+
csrf_tags = [
|
155
|
+
"#{indent} = csrf_meta_tags"
|
156
|
+
]
|
157
|
+
|
158
|
+
# Insert after head tag
|
159
|
+
lines.insert(head_index + 1, *csrf_tags)
|
160
|
+
conversions << 'CSRF meta tags'
|
161
|
+
end
|
162
|
+
|
163
|
+
# Add CSRF token to forms
|
164
|
+
lines.map!.with_index do |line, index|
|
165
|
+
if line =~ /^(\s*)form\[.*method="(post|patch|put|delete)"/i
|
166
|
+
# Mark that this form needs CSRF token
|
167
|
+
conversions << "form CSRF at line #{index + 1}"
|
168
|
+
line
|
169
|
+
else
|
170
|
+
line
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
[lines.join("\n"), conversions]
|
175
|
+
end
|
176
|
+
|
177
|
+
def path_to_rails_helper(href)
|
178
|
+
# Only use custom mappings if provided, no fallbacks
|
179
|
+
return nil unless @custom_mappings
|
180
|
+
|
181
|
+
# Remove leading slash if present for comparison
|
182
|
+
clean_href = href.sub(%r{^/}, '')
|
183
|
+
|
184
|
+
# Try exact match
|
185
|
+
return @custom_mappings[href] if @custom_mappings[href]
|
186
|
+
return @custom_mappings[clean_href] if @custom_mappings[clean_href]
|
187
|
+
|
188
|
+
# Try pattern matching for custom mappings with wildcards
|
189
|
+
@custom_mappings.each do |pattern, helper|
|
190
|
+
if pattern.include?('*')
|
191
|
+
regex_pattern = pattern.gsub('*', '.*')
|
192
|
+
return helper if href.match?(/^#{regex_pattern}$/)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
nil # No mapping found
|
197
|
+
end
|
198
|
+
|
199
|
+
def format_link_options(attrs)
|
200
|
+
return '' if attrs.nil? || attrs.strip.empty?
|
201
|
+
|
202
|
+
# Parse remaining attributes
|
203
|
+
options = []
|
204
|
+
attrs.scan(/(\w+)="([^"]+)"/).each do |key, value|
|
205
|
+
next if key == 'href'
|
206
|
+
|
207
|
+
if key == 'class'
|
208
|
+
options << "class: '#{value}'"
|
209
|
+
elsif key == 'id'
|
210
|
+
options << "id: '#{value}'"
|
211
|
+
elsif key.start_with?('data-')
|
212
|
+
data_key = key.sub('data-', '').gsub('-', '_')
|
213
|
+
options << "data: { #{data_key}: '#{value}' }"
|
214
|
+
else
|
215
|
+
options << "#{key}: '#{value}'"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
options.empty? ? '' : ", #{options.join(", ")}"
|
220
|
+
end
|
221
|
+
|
222
|
+
def format_image_options(attrs)
|
223
|
+
return '' if attrs.nil? || attrs.strip.empty?
|
224
|
+
|
225
|
+
options = []
|
226
|
+
attrs.scan(/(\w+)="([^"]+)"/).each do |key, value|
|
227
|
+
next if key == 'src'
|
228
|
+
|
229
|
+
options << if key == 'alt'
|
230
|
+
"alt: '#{value}'"
|
231
|
+
elsif key == 'class'
|
232
|
+
"class: '#{value}'"
|
233
|
+
elsif %w[width height].include?(key)
|
234
|
+
"#{key}: #{value}"
|
235
|
+
else
|
236
|
+
"#{key}: '#{value}'"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
options.empty? ? '' : ", #{options.join(", ")}"
|
241
|
+
end
|
242
|
+
|
243
|
+
def show_diff(original, modified)
|
244
|
+
puts "\n--- Original ---"
|
245
|
+
puts original[0..500]
|
246
|
+
puts '...' if original.length > 500
|
247
|
+
puts "\n+++ Modified +++"
|
248
|
+
puts modified[0..500]
|
249
|
+
puts '...' if modified.length > 500
|
250
|
+
puts
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require_relative 'slim_manipulator'
|
2
|
+
|
3
|
+
module Blueprint
|
4
|
+
module Html2Slim
|
5
|
+
class SlimValidator < SlimManipulator
|
6
|
+
def validate_file(file_path)
|
7
|
+
content = read_file(file_path)
|
8
|
+
structure = parse_slim_structure(content)
|
9
|
+
|
10
|
+
errors = []
|
11
|
+
warnings = []
|
12
|
+
|
13
|
+
# Check for syntax errors
|
14
|
+
syntax_errors = check_syntax_errors(structure)
|
15
|
+
errors.concat(syntax_errors)
|
16
|
+
|
17
|
+
# Check for common issues that might cause problems
|
18
|
+
potential_issues = check_potential_issues(structure)
|
19
|
+
warnings.concat(potential_issues)
|
20
|
+
|
21
|
+
# Rails-specific checks if requested
|
22
|
+
if options[:check_rails]
|
23
|
+
rails_issues = check_rails_conventions(structure)
|
24
|
+
warnings.concat(rails_issues)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Try to parse with Slim if available
|
28
|
+
if defined?(Slim)
|
29
|
+
begin
|
30
|
+
Slim::Template.new { content }
|
31
|
+
rescue StandardError => e
|
32
|
+
errors << "Slim parsing error: #{e.message}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
{
|
37
|
+
valid: errors.empty?,
|
38
|
+
errors: errors,
|
39
|
+
warnings: warnings
|
40
|
+
}
|
41
|
+
rescue StandardError => e
|
42
|
+
{
|
43
|
+
valid: false,
|
44
|
+
errors: ["Failed to read or validate file: #{e.message}"],
|
45
|
+
warnings: []
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def check_syntax_errors(structure)
|
52
|
+
errors = []
|
53
|
+
indent_stack = [-1]
|
54
|
+
|
55
|
+
structure.each_with_index do |item, _index|
|
56
|
+
line = item[:stripped]
|
57
|
+
indent = item[:indent_level]
|
58
|
+
line_num = item[:line_number]
|
59
|
+
|
60
|
+
# Check indentation consistency
|
61
|
+
if indent > indent_stack.last + 1
|
62
|
+
errors << "Line #{line_num}: Invalid indentation jump (expected #{indent_stack.last + 1}, got #{indent})"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Update indent stack
|
66
|
+
indent_stack.pop while indent_stack.last > indent
|
67
|
+
indent_stack << indent if indent > indent_stack.last
|
68
|
+
|
69
|
+
# Check for invalid syntax patterns
|
70
|
+
if line.start_with?('/') && !line.match?(%r{^/[!|\s]})
|
71
|
+
msg = "Line #{line_num}: Forward slash will be interpreted as comment. "
|
72
|
+
errors << "#{msg}Use pipe notation for text: | #{line}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check for text after element that starts with slash
|
76
|
+
if line =~ %r{^([a-z#.][^\s]*)\s+(/[^/].*)$}i
|
77
|
+
errors << "Line #{line_num}: Forward slash will be interpreted as comment. Use pipe notation for text"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check for unclosed brackets in attributes
|
81
|
+
if line.include?('[') && !line.include?(']')
|
82
|
+
errors << "Line #{line_num}: Unclosed attribute bracket"
|
83
|
+
elsif line.include?(']') && !line.include?('[')
|
84
|
+
errors << "Line #{line_num}: Unexpected closing bracket"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Check for invalid Ruby code markers
|
88
|
+
errors << "Line #{line_num}: Ruby code marker without code" if line =~ /^[=-]\s*$/
|
89
|
+
|
90
|
+
# Check for mixed tabs and spaces (if strict mode)
|
91
|
+
if options[:strict] && item[:line].match?(/\t/)
|
92
|
+
errors << "Line #{line_num}: Contains tabs (use spaces for indentation)"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
errors
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_potential_issues(structure)
|
100
|
+
warnings = []
|
101
|
+
|
102
|
+
structure.each do |item|
|
103
|
+
line = item[:stripped]
|
104
|
+
line_num = item[:line_number]
|
105
|
+
|
106
|
+
# Warn about text that might be misinterpreted
|
107
|
+
warnings << "Line #{line_num}: Text containing '/' might need pipe notation" if line =~ %r{^\w+.*\s/\w+}
|
108
|
+
|
109
|
+
# Warn about very long lines
|
110
|
+
warnings << "Line #{line_num}: Line exceeds 120 characters" if item[:line].length > 120
|
111
|
+
|
112
|
+
# Warn about deprecated Slim syntax
|
113
|
+
warnings << "Line #{line_num}: Single quote for text interpolation is deprecated" if line.start_with?("'")
|
114
|
+
|
115
|
+
# Warn about potential multiline text issues
|
116
|
+
if line =~ /^[a-z#.][^\s]*\s+[^=\-|].{50,}/i
|
117
|
+
warnings << "Line #{line_num}: Long inline text might be better as multiline with pipe notation"
|
118
|
+
end
|
119
|
+
|
120
|
+
# Warn about inline styles (if strict)
|
121
|
+
if options[:strict] && line.include?('style=')
|
122
|
+
warnings << "Line #{line_num}: Inline styles detected (consider using classes)"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
warnings
|
127
|
+
end
|
128
|
+
|
129
|
+
def check_rails_conventions(structure)
|
130
|
+
warnings = []
|
131
|
+
|
132
|
+
structure.each do |item|
|
133
|
+
line = item[:stripped]
|
134
|
+
line_num = item[:line_number]
|
135
|
+
|
136
|
+
# Check for static asset links that should use Rails helpers
|
137
|
+
if line =~ /link.*href=["'].*\.(css|scss)/
|
138
|
+
warnings << "Line #{line_num}: Consider using stylesheet_link_tag for CSS files"
|
139
|
+
end
|
140
|
+
|
141
|
+
if line =~ /script.*src=["'].*\.js/
|
142
|
+
warnings << "Line #{line_num}: Consider using javascript_include_tag for JS files"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Check for static image paths
|
146
|
+
if line =~ %r{img.*src=["'](?!http|//)}
|
147
|
+
warnings << "Line #{line_num}: Consider using image_tag helper for images"
|
148
|
+
end
|
149
|
+
|
150
|
+
# Check for forms without Rails helpers
|
151
|
+
if line.start_with?('form') && !line.include?('form_for') && !line.include?('form_with')
|
152
|
+
warnings << "Line #{line_num}: Consider using Rails form helpers (form_for, form_with)"
|
153
|
+
end
|
154
|
+
|
155
|
+
# Check for missing CSRF token in forms
|
156
|
+
if line =~ /^form\[.*method=["'](post|patch|put|delete)/i
|
157
|
+
warnings << "Line #{line_num}: Ensure CSRF token is included in form"
|
158
|
+
end
|
159
|
+
|
160
|
+
# Check for hardcoded URLs
|
161
|
+
if line =~ %r{href=["']/(users|posts|articles|products)}
|
162
|
+
warnings << "Line #{line_num}: Consider using Rails path helpers instead of hardcoded URLs"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
warnings
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blueprint-html2slim
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Elchinov
|
@@ -127,6 +127,7 @@ email:
|
|
127
127
|
- info@railsblueprint.com
|
128
128
|
executables:
|
129
129
|
- html2slim
|
130
|
+
- slimtool
|
130
131
|
extensions: []
|
131
132
|
extra_rdoc_files: []
|
132
133
|
files:
|
@@ -135,8 +136,15 @@ files:
|
|
135
136
|
- README.md
|
136
137
|
- bin/html2slim
|
137
138
|
- bin/index.html
|
139
|
+
- bin/slimtool
|
138
140
|
- lib/blueprint/html2slim.rb
|
139
141
|
- lib/blueprint/html2slim/converter.rb
|
142
|
+
- lib/blueprint/html2slim/link_extractor.rb
|
143
|
+
- lib/blueprint/html2slim/slim_extractor.rb
|
144
|
+
- lib/blueprint/html2slim/slim_fixer.rb
|
145
|
+
- lib/blueprint/html2slim/slim_manipulator.rb
|
146
|
+
- lib/blueprint/html2slim/slim_railsifier.rb
|
147
|
+
- lib/blueprint/html2slim/slim_validator.rb
|
140
148
|
- lib/blueprint/html2slim/version.rb
|
141
149
|
homepage: https://github.com/railsblueprint/html2slim
|
142
150
|
licenses:
|