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,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herbst
|
|
4
|
+
module Haml
|
|
5
|
+
# Converts the tree produced by Haml::Parser into a Herb AST.
|
|
6
|
+
#
|
|
7
|
+
# The resulting tree prints to ERB that renders the same HTML as the Haml
|
|
8
|
+
# template (modulo insignificant whitespace).
|
|
9
|
+
#
|
|
10
|
+
class Converter < Herbst::Converter
|
|
11
|
+
MID_BLOCK_KEYWORDS = ::Haml::Parser::MID_BLOCK_KEYWORDS - ["end"] #: Array[String]
|
|
12
|
+
ESCAPED_INTERPOLATION = /\AHaml::Util\.escape_html_safe\(\((.*)\)\.to_s\)\z/m #: Regexp
|
|
13
|
+
|
|
14
|
+
# @rbs!
|
|
15
|
+
# type haml_clause = [::Haml::Parser::ParseNode, Array[::Haml::Parser::ParseNode]]
|
|
16
|
+
|
|
17
|
+
# @rbs @options: Hash[Symbol, untyped]
|
|
18
|
+
# @rbs @attributes: Attributes
|
|
19
|
+
# @rbs @filters: Filters
|
|
20
|
+
# @rbs @nuke_inner: Array[bool?]
|
|
21
|
+
|
|
22
|
+
#: (?Hash[Symbol, untyped]) -> void
|
|
23
|
+
def initialize(options = {})
|
|
24
|
+
super()
|
|
25
|
+
|
|
26
|
+
@options = { escape_html: true }.merge(options)
|
|
27
|
+
@attributes = Attributes.new(@warnings)
|
|
28
|
+
@filters = Filters.new(self)
|
|
29
|
+
@nuke_inner = [false]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: (String) -> Herb::AST::DocumentNode
|
|
33
|
+
def convert(source)
|
|
34
|
+
root = ::Haml::Parser.new(@options).call(source)
|
|
35
|
+
children = join(root.children, 0)
|
|
36
|
+
|
|
37
|
+
children << text("\n") unless children.empty?
|
|
38
|
+
|
|
39
|
+
finish(document(children))
|
|
40
|
+
rescue ::Haml::Error => e
|
|
41
|
+
line = e.line
|
|
42
|
+
raise Herbst::Error, line ? "line #{line + 1}: #{e.message}" : e.message
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#: (Array[::Haml::Parser::ParseNode], Integer) -> Array[Herb::AST::Node]
|
|
46
|
+
def join(nodes, depth)
|
|
47
|
+
result = [] #: Array[Herb::AST::Node]
|
|
48
|
+
previous = nil #: ::Haml::Parser::ParseNode?
|
|
49
|
+
|
|
50
|
+
nodes.each do |node|
|
|
51
|
+
converted = convert_node(node, depth)
|
|
52
|
+
converted = [converted] unless converted.is_a?(Array)
|
|
53
|
+
next if converted.empty?
|
|
54
|
+
|
|
55
|
+
result << text(newline(depth)) if previous && whitespace_between?(previous, node)
|
|
56
|
+
result.concat(converted)
|
|
57
|
+
previous = node
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#: (Array[::Haml::Parser::ParseNode], Integer, ?nuke_edges: bool) -> Array[Herb::AST::Node]
|
|
64
|
+
def block_body(nodes, depth, nuke_edges: false)
|
|
65
|
+
children = join(nodes, depth + 1)
|
|
66
|
+
return children if children.empty? || nuke_edges
|
|
67
|
+
|
|
68
|
+
[text(newline(depth + 1)), *children, text(newline(depth))]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#: (String, escape: bool?) -> Array[Herb::AST::Node]
|
|
72
|
+
def interpolate(literal, escape:)
|
|
73
|
+
parts = ::Haml::StringSplitter.compile(literal)
|
|
74
|
+
|
|
75
|
+
parts.flat_map do |type, value|
|
|
76
|
+
next text_nodes(value) if type == :static
|
|
77
|
+
|
|
78
|
+
if (match = value.match(ESCAPED_INTERPOLATION))
|
|
79
|
+
erb(match[1])
|
|
80
|
+
else
|
|
81
|
+
erb(value, opening: escape ? "<%=" : "<%==")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
rescue StandardError
|
|
85
|
+
[erb(literal, opening: escape ? "<%=" : "<%==")]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
#: (::Haml::Parser::ParseNode, Integer) -> (Herb::AST::Node | Array[Herb::AST::Node])
|
|
91
|
+
def convert_node(node, depth)
|
|
92
|
+
case node.type
|
|
93
|
+
when :plain
|
|
94
|
+
text_nodes(node.value[:text])
|
|
95
|
+
when :script
|
|
96
|
+
convert_script(node, depth)
|
|
97
|
+
when :silent_script
|
|
98
|
+
convert_silent_script(node, depth)
|
|
99
|
+
when :tag
|
|
100
|
+
convert_tag(node, depth)
|
|
101
|
+
when :comment
|
|
102
|
+
convert_comment(node, depth)
|
|
103
|
+
when :doctype
|
|
104
|
+
convert_doctype(node)
|
|
105
|
+
when :haml_comment
|
|
106
|
+
convert_haml_comment(node)
|
|
107
|
+
when :filter
|
|
108
|
+
@filters.convert(node, depth)
|
|
109
|
+
else
|
|
110
|
+
raise Herbst::Error, "Unexpected Haml node type: #{node.type}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#: (::Haml::Parser::ParseNode, Integer) -> (Herb::AST::Node | Array[Herb::AST::Node])
|
|
115
|
+
def convert_script(node, depth)
|
|
116
|
+
value = node.value
|
|
117
|
+
opening = value[:escape_html] ? "<%=" : "<%=="
|
|
118
|
+
|
|
119
|
+
if value[:escape_interpolation] || value[:text].include?("Haml::Util.escape_html_safe")
|
|
120
|
+
return interpolate(value[:text], escape: value[:escape_interpolation])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return erb(value[:text], opening: opening) if node.children.empty?
|
|
124
|
+
|
|
125
|
+
erb_block(value[:text], block_body(node.children, depth), opening: opening)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
#: (::Haml::Parser::ParseNode, Integer) -> Herb::AST::Node
|
|
129
|
+
def convert_silent_script(node, depth)
|
|
130
|
+
code = node.value[:text]
|
|
131
|
+
return erb(code, opening: "<%") if node.children.empty?
|
|
132
|
+
|
|
133
|
+
main, clauses = segments(node)
|
|
134
|
+
|
|
135
|
+
converted_clauses = clauses.map do |clause, children|
|
|
136
|
+
[clause.value[:text], clause.value[:keyword], block_body(children, depth), clause.line] #: Herbst::Converter::clause
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
control(code, node.value[:keyword] || keyword(code), block_body(main, depth), converted_clauses, depth, line: node.line)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
#: (::Haml::Parser::ParseNode) -> [Array[::Haml::Parser::ParseNode], Array[haml_clause]]
|
|
143
|
+
def segments(node)
|
|
144
|
+
main = node.children.take_while { |child| !mid_block?(child) }
|
|
145
|
+
groups = node.children.drop(main.size).slice_before { |child| mid_block?(child) }
|
|
146
|
+
clauses = groups.map { |group| [group.fetch(0), group.drop(1)] } #: Array[haml_clause]
|
|
147
|
+
|
|
148
|
+
[main, clauses]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
#: (::Haml::Parser::ParseNode) -> bool
|
|
152
|
+
def mid_block?(node)
|
|
153
|
+
node.type == :silent_script && MID_BLOCK_KEYWORDS.include?(node.value[:keyword])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
#: (::Haml::Parser::ParseNode, Integer) -> (Herb::AST::HTMLElementNode | Array[Herb::AST::Node])
|
|
157
|
+
def convert_tag(node, depth)
|
|
158
|
+
value = node.value
|
|
159
|
+
attributes = @attributes.compile(node)
|
|
160
|
+
self_closing = value[:self_closing] && node.children.empty?
|
|
161
|
+
|
|
162
|
+
body =
|
|
163
|
+
if !node.children.empty?
|
|
164
|
+
with_nuke_inner(value[:nuke_inner_whitespace]) do
|
|
165
|
+
block_body(node.children, depth, nuke_edges: value[:nuke_inner_whitespace])
|
|
166
|
+
end
|
|
167
|
+
elsif value[:value].nil? || value[:value].empty?
|
|
168
|
+
[] #: Array[Herb::AST::Node]
|
|
169
|
+
elsif value[:parse]
|
|
170
|
+
if value[:escape_interpolation] || value[:value].include?("Haml::Util.escape_html_safe")
|
|
171
|
+
interpolate(value[:value], escape: value[:escape_interpolation])
|
|
172
|
+
else
|
|
173
|
+
[erb(value[:value], opening: value[:escape_html] ? "<%=" : "<%==")]
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
text_nodes(value[:value])
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
return [element(value[:name], attributes: attributes), *body] if Herb::HTML::Util.void_element?(value[:name]) && !body.empty?
|
|
180
|
+
|
|
181
|
+
element(value[:name], attributes: attributes, body: body, self_closing: self_closing)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
#: (::Haml::Parser::ParseNode, Integer) -> (Herb::AST::Node | Array[Herb::AST::Node])
|
|
185
|
+
def convert_comment(node, depth)
|
|
186
|
+
value = node.value
|
|
187
|
+
body =
|
|
188
|
+
if !node.children.empty?
|
|
189
|
+
block_body(node.children, depth)
|
|
190
|
+
elsif value[:parse]
|
|
191
|
+
[text(" "), *interpolate(value[:text], escape: true), text(" ")]
|
|
192
|
+
else
|
|
193
|
+
[literal(" #{value[:text]} ")]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if value[:conditional]
|
|
197
|
+
return [text("<!--#{value[:conditional]}><!-->"), *body, text("<!--<![endif]-->")] if value[:revealed]
|
|
198
|
+
|
|
199
|
+
return comment([literal("#{value[:conditional]}>"), *body, literal("<![endif]")])
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
comment(body)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#: (::Haml::Parser::ParseNode) -> (Herb::AST::HTMLDoctypeNode | Array[Herb::AST::Node])
|
|
206
|
+
def convert_doctype(node)
|
|
207
|
+
type = node.value[:type].to_s.downcase
|
|
208
|
+
return [] if type == "xml"
|
|
209
|
+
|
|
210
|
+
doctype(" html")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
#: (::Haml::Parser::ParseNode) -> (Herb::AST::ERBCommentNode | Array[Herb::AST::Node])
|
|
214
|
+
def convert_haml_comment(node)
|
|
215
|
+
text = node.value[:text].to_s
|
|
216
|
+
return [] if text.strip.empty?
|
|
217
|
+
|
|
218
|
+
erb_comment(text.gsub("%>", "% >"))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
#: (::Haml::Parser::ParseNode) -> bool
|
|
222
|
+
def nuke_whitespace?(node)
|
|
223
|
+
node.type == :tag && node.value[:nuke_outer_whitespace]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
#: (::Haml::Parser::ParseNode, ::Haml::Parser::ParseNode) -> bool
|
|
227
|
+
def whitespace_between?(previous, node)
|
|
228
|
+
return false if nuke_whitespace?(previous) || nuke_whitespace?(node)
|
|
229
|
+
return false if previous.type == :script && previous.children.empty? && @nuke_inner.last
|
|
230
|
+
|
|
231
|
+
true
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
#: [T] (bool?) { () -> T } -> T
|
|
235
|
+
def with_nuke_inner(value)
|
|
236
|
+
@nuke_inner.push(value)
|
|
237
|
+
|
|
238
|
+
yield
|
|
239
|
+
ensure
|
|
240
|
+
@nuke_inner.pop
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/escape"
|
|
4
|
+
require "herb"
|
|
5
|
+
|
|
6
|
+
module Herbst
|
|
7
|
+
module Haml
|
|
8
|
+
class Filters
|
|
9
|
+
include Builder
|
|
10
|
+
|
|
11
|
+
# @rbs @converter: Converter
|
|
12
|
+
|
|
13
|
+
#: (Converter) -> void
|
|
14
|
+
def initialize(converter)
|
|
15
|
+
@converter = converter
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: (::Haml::Parser::ParseNode, Integer) -> (Herb::AST::Node | Array[Herb::AST::Node])
|
|
19
|
+
def convert(node, depth)
|
|
20
|
+
name = node.value[:name]
|
|
21
|
+
source = node.value[:text].to_s.rstrip
|
|
22
|
+
|
|
23
|
+
case name
|
|
24
|
+
when "javascript"
|
|
25
|
+
element("script", body: wrapped(source, depth))
|
|
26
|
+
when "css"
|
|
27
|
+
element("style", body: wrapped(source, depth))
|
|
28
|
+
when "plain", "preserve"
|
|
29
|
+
text_nodes(source)
|
|
30
|
+
when "escaped"
|
|
31
|
+
@converter.text_nodes(CGI.escapeHTML(source))
|
|
32
|
+
when "ruby"
|
|
33
|
+
erb(source, opening: "<%")
|
|
34
|
+
when "erb"
|
|
35
|
+
Herb.parse(source).value.children
|
|
36
|
+
when "cdata"
|
|
37
|
+
[text("<![CDATA["), *wrapped(source, depth), text("]]>")]
|
|
38
|
+
else
|
|
39
|
+
raise Herbst::UnsupportedError.new("the `:#{name}` filter is not supported", node.line)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
#: (String, Integer) -> Array[Herb::AST::Node]
|
|
46
|
+
def wrapped(source, depth)
|
|
47
|
+
indent = Converter::INDENT * (depth + 1)
|
|
48
|
+
body = source.gsub(/^(?=.)/, indent)
|
|
49
|
+
|
|
50
|
+
[text("\n"), *text_nodes(body), text("\n#{Converter::INDENT * depth}")]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#: (String) -> Array[Herb::AST::Node]
|
|
54
|
+
def text_nodes(source)
|
|
55
|
+
return @converter.text_nodes(source) unless ::Haml::Util.contains_interpolation?(source)
|
|
56
|
+
|
|
57
|
+
@converter.interpolate(::Haml::Util.unescape_interpolation(source), escape: false)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/herbst/haml.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "haml"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise Herbst::Error, "Converting Haml templates requires the haml gem. Add `gem \"haml\"` to your Gemfile."
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module Herbst
|
|
10
|
+
# Converts Haml templates into Herb's syntax tree and ERB.
|
|
11
|
+
#
|
|
12
|
+
# Herbst::Haml.parse("%p= greeting") # => Herb::AST::DocumentNode
|
|
13
|
+
# Herbst::Haml.to_erb("%p= greeting") # => "<p><%= greeting %></p>\n"
|
|
14
|
+
#
|
|
15
|
+
# To compile Haml, compile its ERB with Herb::Engine:
|
|
16
|
+
#
|
|
17
|
+
# Herb::Engine.new(Herbst::Haml.to_erb(source), escape: true).src
|
|
18
|
+
#
|
|
19
|
+
# Use Herbst::Haml::Converter directly to read the warnings of a conversion. The haml gem is
|
|
20
|
+
# only needed when converting Haml, so it isn't a dependency of Herbst.
|
|
21
|
+
#
|
|
22
|
+
module Haml
|
|
23
|
+
#: (String, **untyped) -> Herb::AST::DocumentNode
|
|
24
|
+
def self.parse(source, **options)
|
|
25
|
+
Converter.new(options).convert(source)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: (String, ?format: bool, **untyped) -> String
|
|
29
|
+
def self.to_erb(source, format: false, **)
|
|
30
|
+
Printer.print(parse(source, **), format: format)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
require_relative "haml/attributes"
|
|
36
|
+
require_relative "haml/filters"
|
|
37
|
+
require_relative "haml/converter"
|