herbst 0.0.1 → 0.1.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/README.md +41 -7
- data/Rakefile +11 -1
- data/exe/herbst +6 -0
- data/herbst.gemspec +5 -3
- data/lib/herbst/builder.rb +358 -0
- data/lib/herbst/cli.rb +276 -0
- data/lib/herbst/converter.rb +182 -0
- data/lib/herbst/haml/attributes.rb +161 -0
- data/lib/herbst/haml/converter.rb +244 -0
- data/lib/herbst/haml/filters.rb +61 -0
- data/lib/herbst/haml.rb +37 -0
- data/lib/herbst/identity_printer.rb +438 -0
- data/lib/herbst/printer.rb +54 -0
- data/lib/herbst/round_trip.rb +67 -0
- data/lib/herbst/slim/converter.rb +452 -0
- data/lib/herbst/slim.rb +30 -0
- data/lib/herbst/version.rb +1 -1
- data/lib/herbst.rb +22 -0
- data/sig/herbst/builder.rbs +110 -0
- data/sig/herbst/cli.rbs +101 -0
- data/sig/herbst/converter.rbs +59 -0
- data/sig/herbst/haml/attributes.rbs +49 -0
- data/sig/herbst/haml/converter.rbs +78 -0
- data/sig/herbst/haml/filters.rbs +25 -0
- data/sig/herbst/haml.rbs +22 -0
- data/sig/herbst/identity_printer.rbs +206 -0
- data/sig/herbst/printer.rbs +25 -0
- data/sig/herbst/round_trip.rbs +26 -0
- data/sig/herbst/slim/converter.rbs +123 -0
- data/sig/herbst/slim.rbs +17 -0
- data/sig/herbst/version.rbs +5 -0
- data/sig/herbst.rbs +11 -1
- metadata +34 -6
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/escape"
|
|
4
|
+
require "herb"
|
|
5
|
+
require "herb/html/util"
|
|
6
|
+
require "temple"
|
|
7
|
+
|
|
8
|
+
module Herbst
|
|
9
|
+
module Slim
|
|
10
|
+
class Converter < Herbst::Converter
|
|
11
|
+
HYPHEN_ATTRIBUTES = ["data", "aria"].freeze #: Array[String]
|
|
12
|
+
|
|
13
|
+
# @rbs @options: Hash[Symbol, untyped]
|
|
14
|
+
|
|
15
|
+
#: (?Hash[Symbol, untyped]) -> void
|
|
16
|
+
def initialize(options = {})
|
|
17
|
+
super()
|
|
18
|
+
|
|
19
|
+
@options = { default_tag: "div" }.merge(options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: (String) -> Herb::AST::DocumentNode
|
|
23
|
+
def convert(source)
|
|
24
|
+
expression = ::Slim::Parser.new(@options).call(source)
|
|
25
|
+
children = join(prepare([expression]), 0)
|
|
26
|
+
|
|
27
|
+
finish(document(children))
|
|
28
|
+
rescue ::Slim::Parser::SyntaxError => e
|
|
29
|
+
raise Herbst::Error, e.message
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
#: (Array[untyped]) -> Array[untyped]
|
|
35
|
+
def prepare(expressions)
|
|
36
|
+
items = expressions.flat_map { |expression| flatten(expression) }
|
|
37
|
+
grouped = [] #: Array[untyped]
|
|
38
|
+
|
|
39
|
+
items.each do |item|
|
|
40
|
+
if control?(item) && item[2].match?(::Slim::EndInserter::END_RE)
|
|
41
|
+
warn(nil, "`- #{item[2].strip}` was dropped, since Slim closes blocks itself")
|
|
42
|
+
elsif clause?(item) && grouped.last&.first == :herbst_control
|
|
43
|
+
grouped.last[3] << item
|
|
44
|
+
elsif control?(item)
|
|
45
|
+
clauses = [] #: Array[untyped]
|
|
46
|
+
grouped << [:herbst_control, item[2], item[3], clauses]
|
|
47
|
+
else
|
|
48
|
+
grouped << item
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
grouped
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: (untyped) -> Array[untyped]
|
|
56
|
+
def flatten(expression)
|
|
57
|
+
return [] if expression.nil? || expression == [:newline]
|
|
58
|
+
return expression.drop(1).flat_map { |child| flatten(child) } if expression.first == :multi
|
|
59
|
+
|
|
60
|
+
[expression]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#: (untyped) -> bool
|
|
64
|
+
def control?(item)
|
|
65
|
+
item[0] == :slim && item[1] == :control
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#: (untyped) -> bool
|
|
69
|
+
def clause?(item)
|
|
70
|
+
control?(item) && item[2].match?(::Slim::EndInserter::ELSE_RE)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
#: (Array[untyped], Integer) -> Array[Herb::AST::Node]
|
|
74
|
+
def join(items, depth)
|
|
75
|
+
result = [] #: Array[Herb::AST::Node]
|
|
76
|
+
previous = nil #: untyped
|
|
77
|
+
|
|
78
|
+
items.each do |item|
|
|
79
|
+
converted = convert_item(item, depth)
|
|
80
|
+
next if converted.empty?
|
|
81
|
+
|
|
82
|
+
result << text(newline(depth)) if previous && (block?(previous) || block?(item))
|
|
83
|
+
result.concat(converted)
|
|
84
|
+
previous = item
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#: (untyped, Integer) -> Array[Herb::AST::Node]
|
|
91
|
+
def body(expression, depth)
|
|
92
|
+
items = prepare([expression])
|
|
93
|
+
children = join(items, depth + 1)
|
|
94
|
+
return children if children.empty?
|
|
95
|
+
|
|
96
|
+
children.unshift(text(newline(depth + 1))) if block?(items.first)
|
|
97
|
+
children.push(text(newline(depth))) if block?(items.last)
|
|
98
|
+
|
|
99
|
+
children
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
#: (untyped) -> bool
|
|
103
|
+
def block?(item)
|
|
104
|
+
return false unless item[0] == :html
|
|
105
|
+
|
|
106
|
+
case item[1]
|
|
107
|
+
when :tag
|
|
108
|
+
!Herb::HTML::Elements::INLINE_ELEMENTS.include?(item[2].to_s)
|
|
109
|
+
when :doctype
|
|
110
|
+
true
|
|
111
|
+
else
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
#: (untyped, Integer) -> Array[Herb::AST::Node]
|
|
117
|
+
def convert_item(item, depth)
|
|
118
|
+
case item[0]
|
|
119
|
+
when :static
|
|
120
|
+
text_nodes(item[1])
|
|
121
|
+
when :html
|
|
122
|
+
convert_html(item, depth)
|
|
123
|
+
when :slim
|
|
124
|
+
convert_slim(item, depth)
|
|
125
|
+
when :herbst_control
|
|
126
|
+
convert_control(item[1], item[2], item[3], depth)
|
|
127
|
+
else
|
|
128
|
+
unexpected(item)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
#: (untyped, Integer) -> Array[Herb::AST::Node]
|
|
133
|
+
def convert_html(item, depth)
|
|
134
|
+
case item[1]
|
|
135
|
+
when :tag
|
|
136
|
+
convert_tag(item, depth)
|
|
137
|
+
when :doctype
|
|
138
|
+
convert_doctype(item[2])
|
|
139
|
+
when :comment
|
|
140
|
+
[comment(convert_text(item[2]))]
|
|
141
|
+
when :condcomment
|
|
142
|
+
[comment([literal("[#{item[2]}]>"), *body(item[3], depth), literal("<![endif]")])]
|
|
143
|
+
else
|
|
144
|
+
unexpected(item)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#: (untyped, Integer) -> Array[Herb::AST::Node]
|
|
149
|
+
def convert_slim(item, depth)
|
|
150
|
+
case item[1]
|
|
151
|
+
when :text
|
|
152
|
+
convert_text(item[3])
|
|
153
|
+
when :interpolate
|
|
154
|
+
interpolate(item[2])
|
|
155
|
+
when :output
|
|
156
|
+
convert_output(item, depth)
|
|
157
|
+
when :embedded
|
|
158
|
+
convert_embedded(item, depth)
|
|
159
|
+
else
|
|
160
|
+
unexpected(item)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
#: (untyped) -> bot
|
|
165
|
+
def unexpected(item)
|
|
166
|
+
raise Herbst::Error, "Unexpected Slim expression: #{item.inspect}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
#: (untyped) -> Array[Herb::AST::Node]
|
|
170
|
+
def convert_text(expression)
|
|
171
|
+
flatten(expression).flat_map do |item|
|
|
172
|
+
item[0] == :slim && item[1] == :interpolate ? interpolate(item[2]) : convert_item(item, 0)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
#: (String, ?escape_static: bool) -> Array[Herb::AST::Node]
|
|
177
|
+
def interpolate(string, escape_static: false)
|
|
178
|
+
parts = ::Slim::Interpolation.new.call([:slim, :interpolate, string])
|
|
179
|
+
|
|
180
|
+
flatten(parts).flat_map do |part|
|
|
181
|
+
if part[0] == :static
|
|
182
|
+
text_nodes(escape_static ? CGI.escapeHTML(part[1]) : part[1])
|
|
183
|
+
else
|
|
184
|
+
[erb(part[3], opening: part[2] ? "<%=" : "<%==")]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
#: (untyped, Integer) -> Array[Herb::AST::Node]
|
|
190
|
+
def convert_tag(item, depth)
|
|
191
|
+
_html, _tag, name, attributes, content = item
|
|
192
|
+
raise Herbst::UnsupportedError, "a splat that sets the tag name, as in `*{tag: ...}`, is not supported" if name == "*"
|
|
193
|
+
|
|
194
|
+
attributes = convert_attributes(attributes)
|
|
195
|
+
|
|
196
|
+
if Herb::HTML::Util.void_element?(name.to_s)
|
|
197
|
+
return [element(name.to_s, attributes: attributes), *join(prepare([content]), depth)]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
children = content.nil? ? [] : body(content, depth) #: Array[Herb::AST::Node]
|
|
201
|
+
|
|
202
|
+
[element(name.to_s, attributes: attributes, body: children, self_closing: content.nil?)]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#: (untyped, Integer) -> Array[Herb::AST::Node]
|
|
206
|
+
def convert_output(item, depth)
|
|
207
|
+
_slim, _output, escape, code, content = item
|
|
208
|
+
code, comment = split_comment(code)
|
|
209
|
+
opening = escape ? "<%=" : "<%=="
|
|
210
|
+
children = body(content, depth)
|
|
211
|
+
|
|
212
|
+
return [*comment_nodes(comment), erb(code, opening: opening)] if children.empty? && !opens_block?(code)
|
|
213
|
+
|
|
214
|
+
[*comment_nodes(comment), erb_block(block_code(code), children, opening: opening)]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
#: (String, untyped, Array[untyped], Integer) -> Array[Herb::AST::Node]
|
|
218
|
+
def convert_control(code, content, clause_items, depth)
|
|
219
|
+
code, comment = split_comment(code)
|
|
220
|
+
return comment_nodes(comment) if code.empty?
|
|
221
|
+
|
|
222
|
+
content, clause_items = nested_clauses(content) if keyword(code) == "case" && clause_items.empty?
|
|
223
|
+
|
|
224
|
+
children = body(content, depth)
|
|
225
|
+
return [*comment_nodes(comment), erb(code, opening: "<%")] if children.empty? && clause_items.empty? && !code.match?(::Slim::EndInserter::IF_RE)
|
|
226
|
+
|
|
227
|
+
clauses = clause_items.map do |clause|
|
|
228
|
+
clause_code, clause_comment = split_comment(clause[2])
|
|
229
|
+
[clause_code, keyword(clause_code), [*comment_nodes(clause_comment), *body(clause[3], depth)], nil] #: Herbst::Converter::clause
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
[*comment_nodes(comment), control(block_code(code), keyword(code), children, clauses, depth)]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Slim also accepts the `- when` lines of a `- case` indented under it.
|
|
236
|
+
#
|
|
237
|
+
#: (untyped) -> [untyped, Array[untyped]]
|
|
238
|
+
def nested_clauses(content)
|
|
239
|
+
items = prepare([content])
|
|
240
|
+
first = items.first
|
|
241
|
+
return [content, []] unless items.size == 1 && first[0] == :herbst_control && ["when", "in"].include?(keyword(first[1]))
|
|
242
|
+
|
|
243
|
+
[[:multi], [[:slim, :control, first[1], first[2]], *first[3]]]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Slim compiles each line of code onto its own line of Ruby, but in ERB the next tag follows on
|
|
247
|
+
# the same line, where a trailing comment would swallow it. The comment moves into an ERB comment.
|
|
248
|
+
#
|
|
249
|
+
#: (String) -> [String, String?]
|
|
250
|
+
def split_comment(code)
|
|
251
|
+
comment = Prism.parse(code).comments.last
|
|
252
|
+
return [code, nil] unless comment && code.byteslice(comment.location.end_offset, code.bytesize).to_s.strip.empty?
|
|
253
|
+
|
|
254
|
+
[code.byteslice(0, comment.location.start_offset).to_s.rstrip, comment.slice.delete_prefix("#").strip]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
#: (String?) -> Array[Herb::AST::Node]
|
|
258
|
+
def comment_nodes(comment)
|
|
259
|
+
comment.nil? || comment.empty? ? [] : [erb_comment(comment.gsub("%>", "% >"))]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
#: (String) -> bool
|
|
263
|
+
def opens_block?(code)
|
|
264
|
+
code.match?(/\bdo\s*(\|[^|]*\|)?\s*\z/)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
#: (String) -> String
|
|
268
|
+
def block_code(code)
|
|
269
|
+
code.match?(::Slim::DoInserter::BLOCK_REGEX) ? code : "#{code} do"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
#: (String) -> Array[Herb::AST::Node]
|
|
273
|
+
def convert_doctype(type)
|
|
274
|
+
html = Temple::HTML::Fast.new(format: :html).call([:html, :doctype, type])[1]
|
|
275
|
+
|
|
276
|
+
Herb.parse(html, track_whitespace: true).value.children
|
|
277
|
+
rescue Temple::FilterError => e
|
|
278
|
+
raise Herbst::UnsupportedError, "`doctype #{type}` is not supported: #{e.message}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
#: (untyped, Integer) -> Array[Herb::AST::Node]
|
|
282
|
+
def convert_embedded(item, depth)
|
|
283
|
+
_slim, _embedded, engine, content, attributes = item
|
|
284
|
+
source = flatten(content).filter_map { |part| part[2] if part[1] == :interpolate }.join
|
|
285
|
+
|
|
286
|
+
case engine
|
|
287
|
+
when "javascript"
|
|
288
|
+
[element("script", attributes: convert_attributes(attributes), body: embedded_body(source, depth))]
|
|
289
|
+
when "css"
|
|
290
|
+
[element("style", attributes: convert_attributes(attributes), body: embedded_body(source, depth))]
|
|
291
|
+
when "ruby"
|
|
292
|
+
[erb(source, opening: "<%")]
|
|
293
|
+
else
|
|
294
|
+
raise Herbst::UnsupportedError, "the `#{engine}:` embedded engine is not supported"
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
#: (String, Integer) -> Array[Herb::AST::Node]
|
|
299
|
+
def embedded_body(source, _depth)
|
|
300
|
+
interpolate(source)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
#: (untyped) -> Array[Herb::AST::Node]
|
|
304
|
+
def convert_attributes(expression)
|
|
305
|
+
values = Hash.new { |hash, key| hash[key] = [] } #: Hash[String, Array[untyped]]
|
|
306
|
+
splats = [] #: Array[String]
|
|
307
|
+
|
|
308
|
+
expression.drop(2).each do |attribute|
|
|
309
|
+
if attribute[1] == :splat
|
|
310
|
+
splats << attribute[2]
|
|
311
|
+
else
|
|
312
|
+
values[attribute[2].to_s] << attribute[3]
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
nodes = values.keys.sort.flat_map do |name|
|
|
317
|
+
name == "class" ? convert_class(values.fetch(name)) : convert_attribute(name, values.fetch(name).fetch(-1))
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
nodes + splats.map do |code|
|
|
321
|
+
warn(nil, "`*#{code}` was converted to `tag.attributes`, which requires Rails 7.1+")
|
|
322
|
+
erb("tag.attributes(**#{code})")
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
#: (Array[untyped]) -> Array[Herb::AST::Node]
|
|
327
|
+
def convert_class(values)
|
|
328
|
+
parts = values.filter_map do |value|
|
|
329
|
+
nodes = class_value(value)
|
|
330
|
+
nodes unless nodes.empty?
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
return [] if parts.empty?
|
|
334
|
+
|
|
335
|
+
[attribute("class", parts.flat_map.with_index { |nodes, index| index.zero? ? nodes : [literal(" "), *nodes] })]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
#: (untyped) -> Array[Herb::AST::Node]
|
|
339
|
+
def class_value(value)
|
|
340
|
+
case value[0]
|
|
341
|
+
when :static
|
|
342
|
+
value[1].to_s.empty? ? [] : [literal(value[1].to_s)]
|
|
343
|
+
when :escape
|
|
344
|
+
attribute_value(value[2][2], value[1])
|
|
345
|
+
when :slim
|
|
346
|
+
_slim, _attrvalue, escape, code = value
|
|
347
|
+
return [literal(static_class(code))] if Temple::StaticAnalyzer.static?(code)
|
|
348
|
+
|
|
349
|
+
code = "#{code}.flatten.map(&:to_s).reject(&:empty?).join(\" \")" if code.strip.start_with?("[")
|
|
350
|
+
[erb(code, opening: escape ? "<%=" : "<%==")]
|
|
351
|
+
else
|
|
352
|
+
[]
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
#: (String, bool) -> Array[Herb::AST::Node]
|
|
357
|
+
def attribute_value(string, escape)
|
|
358
|
+
interpolate(string, escape_static: escape).map do |node|
|
|
359
|
+
node.is_a?(Herb::AST::HTMLTextNode) ? literal(node.content.to_s) : node
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
#: (String) -> String
|
|
364
|
+
def static_class(code)
|
|
365
|
+
value = eval(code) # rubocop:disable Security/Eval
|
|
366
|
+
value = Array(value).flatten.map(&:to_s).reject(&:empty?).join(" ") if value.is_a?(Array)
|
|
367
|
+
|
|
368
|
+
CGI.escapeHTML(value.to_s)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
#: (String, untyped) -> Array[Herb::AST::Node]
|
|
372
|
+
def convert_attribute(name, value)
|
|
373
|
+
case value[0]
|
|
374
|
+
when :static
|
|
375
|
+
[attribute(name, [literal(value[1].to_s)])]
|
|
376
|
+
when :multi
|
|
377
|
+
[attribute(name)]
|
|
378
|
+
when :escape
|
|
379
|
+
[attribute(name, attribute_value(value[2][2], value[1]))]
|
|
380
|
+
when :slim
|
|
381
|
+
if HYPHEN_ATTRIBUTES.include?(name) && value[3].strip.start_with?("{")
|
|
382
|
+
convert_hash_attribute(name, value[2], value[3])
|
|
383
|
+
else
|
|
384
|
+
convert_code_attribute(name, value[2], value[3])
|
|
385
|
+
end
|
|
386
|
+
else
|
|
387
|
+
unexpected(value)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
#: (String, bool, String) -> Array[Herb::AST::Node]
|
|
392
|
+
def convert_hash_attribute(name, escape, code)
|
|
393
|
+
entries = hash_entries(code)
|
|
394
|
+
|
|
395
|
+
unless entries
|
|
396
|
+
warn(nil, "`#{name}=#{code}` was converted to `tag.attributes`, which requires Rails 7.1+")
|
|
397
|
+
return [erb("tag.attributes(#{name}: #{code})")]
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
entries.flat_map do |key, value|
|
|
401
|
+
if value.strip.start_with?("{")
|
|
402
|
+
convert_hash_attribute("#{name}-#{key}", escape, value)
|
|
403
|
+
else
|
|
404
|
+
convert_code_attribute("#{name}-#{key}", escape, value)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
#: (String) -> Array[[String, String]]?
|
|
410
|
+
def hash_entries(code)
|
|
411
|
+
hash = Prism.parse(code).value.statements.body.first
|
|
412
|
+
return nil unless hash.is_a?(Prism::HashNode)
|
|
413
|
+
|
|
414
|
+
entries = [] #: Array[[String, String]]
|
|
415
|
+
|
|
416
|
+
hash.elements.each do |element|
|
|
417
|
+
return nil unless element.is_a?(Prism::AssocNode)
|
|
418
|
+
|
|
419
|
+
key = element.key
|
|
420
|
+
return nil unless key.is_a?(Prism::SymbolNode) || key.is_a?(Prism::StringNode)
|
|
421
|
+
|
|
422
|
+
entries << [key.unescaped, element.value.slice]
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
entries
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
#: (String, bool, String) -> Array[Herb::AST::Node]
|
|
429
|
+
def convert_code_attribute(name, escape, code)
|
|
430
|
+
case code.strip
|
|
431
|
+
when "true"
|
|
432
|
+
[attribute(name)]
|
|
433
|
+
when "false", "nil"
|
|
434
|
+
[]
|
|
435
|
+
else
|
|
436
|
+
if Temple::StaticAnalyzer.static?(code)
|
|
437
|
+
value = eval(code) # rubocop:disable Security/Eval
|
|
438
|
+
return convert_code_attribute(name, escape, value.inspect) if [true, false, nil].include?(value)
|
|
439
|
+
|
|
440
|
+
return [attribute(name, [literal(escape ? CGI.escapeHTML(value.to_s) : value.to_s)])]
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
if Herb::HTML::Util.boolean_attribute?(name)
|
|
444
|
+
[erb("#{name.inspect} if #{code}")]
|
|
445
|
+
else
|
|
446
|
+
[attribute(name, [erb(code, opening: escape ? "<%=" : "<%==")])]
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
data/lib/herbst/slim.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "slim"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise Herbst::Error, "Converting Slim templates requires the slim gem. Add `gem \"slim\"` to your Gemfile."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module Herbst
|
|
10
|
+
# Converts Slim templates into Herb's syntax tree and ERB.
|
|
11
|
+
#
|
|
12
|
+
# Herbst::Slim.parse("p = greeting") # => Herb::AST::DocumentNode
|
|
13
|
+
# Herbst::Slim.to_erb("p = greeting") # => "<p><%= greeting %></p>"
|
|
14
|
+
#
|
|
15
|
+
# The slim gem is only needed when converting Slim, so it isn't a dependency of Herbst.
|
|
16
|
+
#
|
|
17
|
+
module Slim
|
|
18
|
+
#: (String, **untyped) -> Herb::AST::DocumentNode
|
|
19
|
+
def self.parse(source, **options)
|
|
20
|
+
Converter.new(options).convert(source)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#: (String, ?format: bool, **untyped) -> String
|
|
24
|
+
def self.to_erb(source, format: false, **)
|
|
25
|
+
Printer.print(parse(source, **), format: format)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require_relative "slim/converter"
|
data/lib/herbst/version.rb
CHANGED
data/lib/herbst.rb
CHANGED
|
@@ -3,4 +3,26 @@
|
|
|
3
3
|
require_relative "herbst/version"
|
|
4
4
|
|
|
5
5
|
module Herbst
|
|
6
|
+
autoload :Haml, "#{__dir__}/herbst/haml"
|
|
7
|
+
autoload :Slim, "#{__dir__}/herbst/slim"
|
|
8
|
+
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class UnsupportedError < Error
|
|
12
|
+
attr_reader :line #: Integer?
|
|
13
|
+
|
|
14
|
+
#: (String, ?Integer?) -> void
|
|
15
|
+
def initialize(message, line = nil)
|
|
16
|
+
@line = line
|
|
17
|
+
|
|
18
|
+
super(line ? "line #{line}: #{message}" : message)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
6
21
|
end
|
|
22
|
+
|
|
23
|
+
require_relative "herbst/builder"
|
|
24
|
+
require_relative "herbst/identity_printer"
|
|
25
|
+
require_relative "herbst/printer"
|
|
26
|
+
require_relative "herbst/round_trip"
|
|
27
|
+
require_relative "herbst/converter"
|
|
28
|
+
require_relative "herbst/cli"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Generated from lib/herbst/builder.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Herbst
|
|
4
|
+
module Builder
|
|
5
|
+
# : (String, String) -> Herb::Token
|
|
6
|
+
def token: (String, String) -> Herb::Token
|
|
7
|
+
|
|
8
|
+
# : (Array[Herb::AST::Node]) -> Herb::AST::DocumentNode
|
|
9
|
+
def document: (Array[Herb::AST::Node]) -> Herb::AST::DocumentNode
|
|
10
|
+
|
|
11
|
+
# : (String) -> Herb::AST::HTMLTextNode
|
|
12
|
+
def text: (String) -> Herb::AST::HTMLTextNode
|
|
13
|
+
|
|
14
|
+
# : (String) -> Herb::AST::LiteralNode
|
|
15
|
+
def literal: (String) -> Herb::AST::LiteralNode
|
|
16
|
+
|
|
17
|
+
# : (?String) -> Herb::AST::WhitespaceNode
|
|
18
|
+
def whitespace: (?String) -> Herb::AST::WhitespaceNode
|
|
19
|
+
|
|
20
|
+
# : (String, ?opening: String) -> (Herb::AST::ERBContentNode | Herb::AST::ERBYieldNode)
|
|
21
|
+
def erb: (String, ?opening: String) -> (Herb::AST::ERBContentNode | Herb::AST::ERBYieldNode)
|
|
22
|
+
|
|
23
|
+
# : (String) -> bool
|
|
24
|
+
def yield?: (String) -> bool
|
|
25
|
+
|
|
26
|
+
# : (String) -> Herb::AST::ERBCommentNode
|
|
27
|
+
def erb_comment: (String) -> Herb::AST::ERBCommentNode
|
|
28
|
+
|
|
29
|
+
# : () -> Herb::AST::ERBEndNode
|
|
30
|
+
def erb_end: () -> Herb::AST::ERBEndNode
|
|
31
|
+
|
|
32
|
+
# : (String, Array[Herb::AST::Node], ?subsequent: (Herb::AST::ERBIfNode | Herb::AST::ERBElseNode)?, ?end_node: Herb::AST::ERBEndNode?) -> Herb::AST::ERBIfNode
|
|
33
|
+
def erb_if: (String, Array[Herb::AST::Node], ?subsequent: (Herb::AST::ERBIfNode | Herb::AST::ERBElseNode)?, ?end_node: Herb::AST::ERBEndNode?) -> Herb::AST::ERBIfNode
|
|
34
|
+
|
|
35
|
+
# : (String, Array[Herb::AST::Node], ?else_clause: Herb::AST::ERBElseNode?) -> Herb::AST::ERBUnlessNode
|
|
36
|
+
def erb_unless: (String, Array[Herb::AST::Node], ?else_clause: Herb::AST::ERBElseNode?) -> Herb::AST::ERBUnlessNode
|
|
37
|
+
|
|
38
|
+
# : (Array[Herb::AST::Node]) -> Herb::AST::ERBElseNode
|
|
39
|
+
def erb_else: (Array[Herb::AST::Node]) -> Herb::AST::ERBElseNode
|
|
40
|
+
|
|
41
|
+
# : (String, Array[Herb::AST::ERBWhenNode], ?else_clause: Herb::AST::ERBElseNode?) -> Herb::AST::ERBCaseNode
|
|
42
|
+
def erb_case: (String, Array[Herb::AST::ERBWhenNode], ?else_clause: Herb::AST::ERBElseNode?) -> Herb::AST::ERBCaseNode
|
|
43
|
+
|
|
44
|
+
# : (String, Array[Herb::AST::Node]) -> Herb::AST::ERBWhenNode
|
|
45
|
+
def erb_when: (String, Array[Herb::AST::Node]) -> Herb::AST::ERBWhenNode
|
|
46
|
+
|
|
47
|
+
# : (String, Array[Herb::AST::Node], ?rescue_clause: Herb::AST::ERBRescueNode?, ?else_clause: Herb::AST::ERBElseNode?, ?ensure_clause: Herb::AST::ERBEnsureNode?) -> Herb::AST::ERBBeginNode
|
|
48
|
+
def erb_begin: (String, Array[Herb::AST::Node], ?rescue_clause: Herb::AST::ERBRescueNode?, ?else_clause: Herb::AST::ERBElseNode?, ?ensure_clause: Herb::AST::ERBEnsureNode?) -> Herb::AST::ERBBeginNode
|
|
49
|
+
|
|
50
|
+
# : (String, Array[Herb::AST::Node], ?subsequent: Herb::AST::ERBRescueNode?) -> Herb::AST::ERBRescueNode
|
|
51
|
+
def erb_rescue: (String, Array[Herb::AST::Node], ?subsequent: Herb::AST::ERBRescueNode?) -> Herb::AST::ERBRescueNode
|
|
52
|
+
|
|
53
|
+
# : (Array[Herb::AST::Node]) -> Herb::AST::ERBEnsureNode
|
|
54
|
+
def erb_ensure: (Array[Herb::AST::Node]) -> Herb::AST::ERBEnsureNode
|
|
55
|
+
|
|
56
|
+
# : (String, Array[Herb::AST::Node]) -> Herb::AST::ERBWhileNode
|
|
57
|
+
def erb_while: (String, Array[Herb::AST::Node]) -> Herb::AST::ERBWhileNode
|
|
58
|
+
|
|
59
|
+
# : (String, Array[Herb::AST::Node]) -> Herb::AST::ERBUntilNode
|
|
60
|
+
def erb_until: (String, Array[Herb::AST::Node]) -> Herb::AST::ERBUntilNode
|
|
61
|
+
|
|
62
|
+
# : (String, Array[Herb::AST::Node]) -> Herb::AST::ERBForNode
|
|
63
|
+
def erb_for: (String, Array[Herb::AST::Node]) -> Herb::AST::ERBForNode
|
|
64
|
+
|
|
65
|
+
# : (String, Array[Herb::AST::Node], ?opening: String) -> Herb::AST::ERBBlockNode
|
|
66
|
+
def erb_block: (String, Array[Herb::AST::Node], ?opening: String) -> Herb::AST::ERBBlockNode
|
|
67
|
+
|
|
68
|
+
# : (String) -> Array[Herb::AST::RubyParameterNode]
|
|
69
|
+
def block_arguments: (String) -> Array[Herb::AST::RubyParameterNode]
|
|
70
|
+
|
|
71
|
+
# : (Prism::node) -> Herb::AST::RubyParameterNode?
|
|
72
|
+
def ruby_parameter: (Prism::node) -> Herb::AST::RubyParameterNode?
|
|
73
|
+
|
|
74
|
+
# : (Symbol?, String, bool, ?Prism::node?) -> Herb::AST::RubyParameterNode?
|
|
75
|
+
def ruby_parameter_node: (Symbol?, String, bool, ?Prism::node?) -> Herb::AST::RubyParameterNode?
|
|
76
|
+
|
|
77
|
+
# : (String, ?attributes: Array[Herb::AST::Node], ?body: Array[Herb::AST::Node], ?self_closing: bool) -> Herb::AST::HTMLElementNode
|
|
78
|
+
def element: (String, ?attributes: Array[Herb::AST::Node], ?body: Array[Herb::AST::Node], ?self_closing: bool) -> Herb::AST::HTMLElementNode
|
|
79
|
+
|
|
80
|
+
# : (String | Herb::AST::ERBContentNode, ?Array[Herb::AST::Node]?) -> Herb::AST::HTMLAttributeNode
|
|
81
|
+
def attribute: (String | Herb::AST::ERBContentNode, ?Array[Herb::AST::Node]?) -> Herb::AST::HTMLAttributeNode
|
|
82
|
+
|
|
83
|
+
# : (Array[Herb::AST::Node]) -> Herb::AST::HTMLCommentNode
|
|
84
|
+
def comment: (Array[Herb::AST::Node]) -> Herb::AST::HTMLCommentNode
|
|
85
|
+
|
|
86
|
+
# : (String) -> Herb::AST::HTMLDoctypeNode
|
|
87
|
+
def doctype: (String) -> Herb::AST::HTMLDoctypeNode
|
|
88
|
+
|
|
89
|
+
# : (String) -> Herb::AST::XMLDeclarationNode
|
|
90
|
+
def xml_declaration: (String) -> Herb::AST::XMLDeclarationNode
|
|
91
|
+
|
|
92
|
+
# : (Prism::node) -> Array[Prism::node]
|
|
93
|
+
def destructured: (Prism::node) -> Array[Prism::node]
|
|
94
|
+
|
|
95
|
+
# : (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
96
|
+
def open_tag_children: (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
97
|
+
|
|
98
|
+
# : [T < Herb::AST::Node] (T) -> T
|
|
99
|
+
def merge_texts!: [T < Herb::AST::Node] (T) -> T
|
|
100
|
+
|
|
101
|
+
# : (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
102
|
+
def merge_adjacent_texts: (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
103
|
+
|
|
104
|
+
# : (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
105
|
+
def merge_literals: (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
106
|
+
|
|
107
|
+
# : (String, String) -> { tag_opening: Herb::Token, content: Herb::Token, tag_closing: Herb::Token }
|
|
108
|
+
def erb_tag: (String, String) -> { tag_opening: Herb::Token, content: Herb::Token, tag_closing: Herb::Token }
|
|
109
|
+
end
|
|
110
|
+
end
|