blueprint-html2slim 1.0.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 +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +136 -0
- data/bin/html2slim +216 -0
- data/bin/index.html +545 -0
- data/lib/blueprint/html2slim/converter.rb +338 -0
- data/lib/blueprint/html2slim/version.rb +5 -0
- data/lib/blueprint/html2slim.rb +7 -0
- metadata +166 -0
@@ -0,0 +1,338 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'erubi'
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
module Blueprint
|
6
|
+
module Html2Slim
|
7
|
+
class Converter
|
8
|
+
VOID_ELEMENTS = %w[area base br col embed hr img input link meta param source track wbr].freeze
|
9
|
+
INLINE_ELEMENTS = %w[a abbr b bdo br cite code dfn em i kbd mark q s samp small span strong sub sup time u
|
10
|
+
var].freeze
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@options = options
|
14
|
+
@indent_size = options[:indent_size] || 2
|
15
|
+
@erb_pattern = /<%(-|=)?(.+?)(-)?%>/m
|
16
|
+
end
|
17
|
+
|
18
|
+
def convert(html_content)
|
19
|
+
lines = []
|
20
|
+
|
21
|
+
# Handle DOCTYPE declaration
|
22
|
+
if html_content =~ /<!DOCTYPE\s+(.+?)>/i
|
23
|
+
doctype_content = ::Regexp.last_match(1)
|
24
|
+
if doctype_content =~ /strict/i
|
25
|
+
lines << "doctype strict"
|
26
|
+
elsif doctype_content =~ /transitional/i
|
27
|
+
lines << "doctype transitional"
|
28
|
+
elsif doctype_content =~ /frameset/i
|
29
|
+
lines << "doctype frameset"
|
30
|
+
elsif doctype_content =~ /html$/i
|
31
|
+
lines << "doctype html"
|
32
|
+
else
|
33
|
+
lines << "doctype"
|
34
|
+
end
|
35
|
+
# Remove DOCTYPE from content for further processing
|
36
|
+
html_content = html_content.sub(/<!DOCTYPE\s+.+?>/i, '')
|
37
|
+
end
|
38
|
+
|
39
|
+
html_content = preprocess_erb(html_content)
|
40
|
+
# Use HTML.parse for full documents, DocumentFragment for fragments
|
41
|
+
if html_content =~ /<html/i
|
42
|
+
doc = Nokogiri::HTML.parse(html_content)
|
43
|
+
# Process the html element if it exists
|
44
|
+
if html_element = doc.at('html')
|
45
|
+
node_lines = process_node(html_element, 0)
|
46
|
+
lines.concat(node_lines) unless node_lines.empty?
|
47
|
+
else
|
48
|
+
doc.root.children.each do |node|
|
49
|
+
node_lines = process_node(node, 0)
|
50
|
+
lines.concat(node_lines) unless node_lines.empty?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(html_content)
|
55
|
+
doc.children.each do |node|
|
56
|
+
node_lines = process_node(node, 0)
|
57
|
+
lines.concat(node_lines) unless node_lines.empty?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
result = lines.join("\n")
|
61
|
+
# Ensure the result ends with a newline
|
62
|
+
result += "\n" unless result.end_with?("\n")
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def preprocess_erb(content)
|
69
|
+
# Convert ERB blocks to span elements to preserve hierarchy
|
70
|
+
# This approach is inspired by the original html2slim gem
|
71
|
+
|
72
|
+
# Keep ERB tags in attributes unchanged by temporarily replacing them
|
73
|
+
erb_in_attrs = []
|
74
|
+
content = content.gsub(/(<[^>]*)(<%=?.+?%>)([^>]*>)/) do
|
75
|
+
before = ::Regexp.last_match(1)
|
76
|
+
erb = ::Regexp.last_match(2)
|
77
|
+
after = ::Regexp.last_match(3)
|
78
|
+
placeholder = "ERB_IN_ATTR_#{erb_in_attrs.length}"
|
79
|
+
erb_in_attrs << erb
|
80
|
+
"#{before}#{placeholder}#{after}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Handle multi-line ERB blocks first (with m flag for multiline)
|
84
|
+
content = content.gsub(/<%\s*\n(.*?)\n\s*-?%>/m) do
|
85
|
+
code_lines = ::Regexp.last_match(1).strip.split("\n")
|
86
|
+
# Convert to multiple single-line ERB comments
|
87
|
+
code_lines.map { |line| "<!--ERB_CODE:#{line.strip}-->" }.join("\n")
|
88
|
+
end
|
89
|
+
|
90
|
+
# Convert simple ERB output tags that don't create blocks
|
91
|
+
# This prevents them from being caught by the block regex
|
92
|
+
content = content.gsub(/<%=\s*([^%]+?)\s*%>/) do
|
93
|
+
code = ::Regexp.last_match(1).strip
|
94
|
+
# Skip if it's a do block
|
95
|
+
if code =~ /\bdo\s*(\|[^|]*\|)?\s*$/
|
96
|
+
"<%= #{code} %>" # Keep original, will be processed later
|
97
|
+
else
|
98
|
+
%(<!--ERB_OUTPUT:#{code}-->)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Convert ERB blocks that create structure (do...end, if...end, etc.)
|
103
|
+
# 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
|
105
|
+
type = ::Regexp.last_match(1)
|
106
|
+
code = ::Regexp.last_match(2).strip
|
107
|
+
# Preserve whether it was = or - in the code attribute
|
108
|
+
prefix = type == '=' ? '=' : ''
|
109
|
+
%(<span erb-code="#{prefix}#{code.gsub('"', '"')}">)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Handle else
|
113
|
+
content = content.gsub(/<%-?\s*else\s*-?%>/, %(</span><span erb-code="else">))
|
114
|
+
|
115
|
+
# Handle elsif
|
116
|
+
content = content.gsub(/<%-?\s*(elsif .+?)\s*-?%>/) do
|
117
|
+
code = ::Regexp.last_match(1).strip
|
118
|
+
%(</span><span erb-code="#{code.gsub('"', '"')}">)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Handle when
|
122
|
+
content = content.gsub(/<%-?\s*(when .+?)\s*-?%>/) do
|
123
|
+
code = ::Regexp.last_match(1).strip
|
124
|
+
%(</span><span erb-code="#{code.gsub('"', '"')}">)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Handle end statements - close the span
|
128
|
+
content = content.gsub(/<%\s*(end|}|end\s+-)\s*%>/, %(</span>))
|
129
|
+
|
130
|
+
# Convert any remaining ERB code tags to comments
|
131
|
+
content = content.gsub(/<%-?\s*(.+?)\s*%>/) do
|
132
|
+
code = ::Regexp.last_match(1).strip
|
133
|
+
%(<!--ERB_CODE:#{code}-->)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Restore ERB tags in attributes
|
137
|
+
erb_in_attrs.each_with_index do |erb, i|
|
138
|
+
content = content.gsub("ERB_IN_ATTR_#{i}", erb)
|
139
|
+
end
|
140
|
+
|
141
|
+
content
|
142
|
+
end
|
143
|
+
|
144
|
+
def process_node(node, depth)
|
145
|
+
case node
|
146
|
+
when Nokogiri::XML::Element
|
147
|
+
process_element(node, depth)
|
148
|
+
when Nokogiri::XML::Text
|
149
|
+
process_text(node, depth)
|
150
|
+
when Nokogiri::XML::Comment
|
151
|
+
process_comment(node, depth)
|
152
|
+
else
|
153
|
+
[]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def process_element(node, depth)
|
158
|
+
lines = []
|
159
|
+
indent = ' ' * (depth * @indent_size)
|
160
|
+
|
161
|
+
# Check if this is an ERB span element
|
162
|
+
if node.name == 'span' && node['erb-code']
|
163
|
+
erb_code = node['erb-code'].gsub('"', '"')
|
164
|
+
|
165
|
+
# Determine if it's output (=) or code (-)
|
166
|
+
if erb_code =~ /^(if|unless|case|for|while|elsif|else|when)\b/
|
167
|
+
lines << "#{indent}- #{erb_code}"
|
168
|
+
elsif erb_code.start_with?('=')
|
169
|
+
# It was originally <%= ... %>, use = prefix
|
170
|
+
lines << "#{indent}= #{erb_code[1..-1].strip}"
|
171
|
+
else
|
172
|
+
# It was originally <% ... %>, use - prefix
|
173
|
+
lines << "#{indent}- #{erb_code}"
|
174
|
+
end
|
175
|
+
|
176
|
+
# Process children with increased depth
|
177
|
+
node.children.each do |child|
|
178
|
+
child_lines = process_node(child, depth + 1)
|
179
|
+
lines.concat(child_lines) unless child_lines.empty?
|
180
|
+
end
|
181
|
+
|
182
|
+
return lines
|
183
|
+
end
|
184
|
+
|
185
|
+
tag_line = build_tag_line(node, depth)
|
186
|
+
|
187
|
+
if VOID_ELEMENTS.include?(node.name.downcase)
|
188
|
+
lines << "#{indent}#{tag_line}"
|
189
|
+
elsif node.children.empty?
|
190
|
+
lines << "#{indent}#{tag_line}"
|
191
|
+
elsif single_text_child?(node)
|
192
|
+
text = node.children.first.text
|
193
|
+
if text.strip.empty?
|
194
|
+
lines << "#{indent}#{tag_line}"
|
195
|
+
elsif node.name.downcase == 'pre'
|
196
|
+
# Preserve whitespace in pre tags but still strip leading/trailing
|
197
|
+
text = text.strip.gsub(/\n\s*/, '\n ')
|
198
|
+
lines << "#{indent}#{tag_line} #{text}"
|
199
|
+
elsif text.include?("\n") && text.strip.lines.count > 1
|
200
|
+
# Multiline text - use pipe notation
|
201
|
+
lines << "#{indent}#{tag_line}"
|
202
|
+
text.strip.lines.each do |line|
|
203
|
+
lines << "#{" " * ((depth + 1) * @indent_size)}| #{line.strip}" unless line.strip.empty?
|
204
|
+
end
|
205
|
+
else
|
206
|
+
text = process_inline_text(text.strip)
|
207
|
+
if text.empty?
|
208
|
+
lines << "#{indent}#{tag_line}"
|
209
|
+
else
|
210
|
+
lines << "#{indent}#{tag_line} #{text}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
else
|
214
|
+
lines << "#{indent}#{tag_line}"
|
215
|
+
node.children.each do |child|
|
216
|
+
child_lines = process_node(child, depth + 1)
|
217
|
+
lines.concat(child_lines) unless child_lines.empty?
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
lines
|
222
|
+
end
|
223
|
+
|
224
|
+
def build_tag_line(node, _depth)
|
225
|
+
tag = node.name
|
226
|
+
id = node['id']
|
227
|
+
# Strip and split classes, filtering out empty strings
|
228
|
+
classes = node['class']&.strip&.split(/\s+/)&.reject(&:empty?) || []
|
229
|
+
attributes = collect_attributes(node)
|
230
|
+
|
231
|
+
# Treat empty id as no id
|
232
|
+
id = nil if id && id.strip.empty?
|
233
|
+
|
234
|
+
line = if tag.downcase == 'div' && (id || !classes.empty?)
|
235
|
+
''
|
236
|
+
else
|
237
|
+
tag
|
238
|
+
end
|
239
|
+
|
240
|
+
line += "##{id}" if id
|
241
|
+
|
242
|
+
classes.each do |cls|
|
243
|
+
line += ".#{cls}"
|
244
|
+
end
|
245
|
+
|
246
|
+
unless attributes.empty?
|
247
|
+
line = 'div' if line.empty?
|
248
|
+
line += build_attribute_string(attributes)
|
249
|
+
end
|
250
|
+
|
251
|
+
line = tag if line.empty?
|
252
|
+
line
|
253
|
+
end
|
254
|
+
|
255
|
+
def collect_attributes(node)
|
256
|
+
attributes = {}
|
257
|
+
node.attributes.each do |name, attr|
|
258
|
+
next if %w[id class].include?(name)
|
259
|
+
|
260
|
+
attributes[name] = attr.value
|
261
|
+
end
|
262
|
+
attributes
|
263
|
+
end
|
264
|
+
|
265
|
+
def build_attribute_string(attributes)
|
266
|
+
return '' if attributes.empty?
|
267
|
+
|
268
|
+
attr_parts = attributes.map do |key, value|
|
269
|
+
if value.nil? || value == ''
|
270
|
+
key
|
271
|
+
elsif value.include?('"')
|
272
|
+
"#{key}='#{value}'"
|
273
|
+
else
|
274
|
+
"#{key}=\"#{value}\""
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
"[#{attr_parts.join(" ")}]"
|
279
|
+
end
|
280
|
+
|
281
|
+
def process_text(node, depth)
|
282
|
+
text = node.text
|
283
|
+
return [] if text.strip.empty? && !text.include?("\n")
|
284
|
+
|
285
|
+
indent = ' ' * (depth * @indent_size)
|
286
|
+
processed_text = process_inline_text(text.strip)
|
287
|
+
return [] if processed_text.empty?
|
288
|
+
|
289
|
+
if processed_text.include?("\n")
|
290
|
+
processed_text.split("\n").map { |line| "#{indent}| #{line}" }
|
291
|
+
else
|
292
|
+
["#{indent}| #{processed_text}"]
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def process_comment(node, depth)
|
297
|
+
comment_text = node.text.strip
|
298
|
+
|
299
|
+
# Extract indentation level if present
|
300
|
+
extra_indent = 0
|
301
|
+
if comment_text =~ /:INDENT:(\d+)$/
|
302
|
+
extra_indent = $1.to_i
|
303
|
+
comment_text = comment_text.sub(/:INDENT:\d+$/, '')
|
304
|
+
end
|
305
|
+
|
306
|
+
total_depth = depth + extra_indent
|
307
|
+
indent = ' ' * (total_depth * @indent_size)
|
308
|
+
|
309
|
+
if comment_text.start_with?('ERB_OUTPUT_BLOCK:')
|
310
|
+
erb_content = comment_text.sub('ERB_OUTPUT_BLOCK:', '')
|
311
|
+
["#{indent}= #{erb_content}"]
|
312
|
+
elsif comment_text.start_with?('ERB_OUTPUT:')
|
313
|
+
erb_content = comment_text.sub('ERB_OUTPUT:', '')
|
314
|
+
["#{indent}= #{erb_content}"]
|
315
|
+
elsif comment_text.start_with?('ERB_CODE_BLOCK:')
|
316
|
+
erb_content = comment_text.sub('ERB_CODE_BLOCK:', '')
|
317
|
+
["#{indent}- #{erb_content}"]
|
318
|
+
elsif comment_text.start_with?('ERB_CODE:')
|
319
|
+
erb_content = comment_text.sub('ERB_CODE:', '')
|
320
|
+
["#{indent}- #{erb_content}"]
|
321
|
+
elsif comment_text == 'ERB_END'
|
322
|
+
# Don't output 'end' statements in Slim
|
323
|
+
[]
|
324
|
+
else
|
325
|
+
["#{indent}/! #{comment_text}"]
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def process_inline_text(text)
|
330
|
+
text.gsub(/\s+/, ' ').strip
|
331
|
+
end
|
332
|
+
|
333
|
+
def single_text_child?(node)
|
334
|
+
node.children.size == 1 && node.children.first.text?
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
metadata
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: blueprint-html2slim
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vladimir Elchinov
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: erubi
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.12'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '1.12'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: nokogiri
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.16'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.16'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: thor
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.3'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.3'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rake
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '13.0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '13.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rspec
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.12'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.12'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rubocop
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.50'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '1.50'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rubocop-rake
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.6'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.6'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rubocop-rspec
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '2.22'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '2.22'
|
124
|
+
description: A Ruby command-line tool to convert HTML and ERB files to Slim format
|
125
|
+
with smart naming conventions and backup options
|
126
|
+
email:
|
127
|
+
- info@railsblueprint.com
|
128
|
+
executables:
|
129
|
+
- html2slim
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- CHANGELOG.md
|
134
|
+
- LICENSE
|
135
|
+
- README.md
|
136
|
+
- bin/html2slim
|
137
|
+
- bin/index.html
|
138
|
+
- lib/blueprint/html2slim.rb
|
139
|
+
- lib/blueprint/html2slim/converter.rb
|
140
|
+
- lib/blueprint/html2slim/version.rb
|
141
|
+
homepage: https://github.com/railsblueprint/html2slim
|
142
|
+
licenses:
|
143
|
+
- MIT
|
144
|
+
metadata:
|
145
|
+
homepage_uri: https://github.com/railsblueprint/html2slim
|
146
|
+
source_code_uri: https://github.com/railsblueprint/html2slim
|
147
|
+
changelog_uri: https://github.com/railsblueprint/html2slim/blob/main/CHANGELOG.md
|
148
|
+
rubygems_mfa_required: 'true'
|
149
|
+
rdoc_options: []
|
150
|
+
require_paths:
|
151
|
+
- lib
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: 2.7.0
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
requirements: []
|
163
|
+
rubygems_version: 3.6.7
|
164
|
+
specification_version: 4
|
165
|
+
summary: Convert HTML and ERB files to Slim format
|
166
|
+
test_files: []
|