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