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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97c0f8a85d40558521510f33882058615c8530334598498b8db1407d799655d2
|
|
4
|
+
data.tar.gz: 917a913efabe6050570792a30fb49a12e6e9086b8e7ccf3fac90c2c35563f533
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa0044094d6f1fac7a519a105bcf848bc40ed2568039eef3c6288e4d23bb66b39c12c0d8f6006c6b284c7afb6a062a69aa0d612323c17d87eceef08c43fbe6ec
|
|
7
|
+
data.tar.gz: 56799703b87cfdef92316360560be4939c0f0d6cbcd9f25ff08c7f5d6d3dbfefe6a467fe6b18457eecc25f5fec02b94fbcda9ac9e5fe17ccfa691da6a94ae4fb
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<h2 align="center"
|
|
1
|
+
<h2 align="center">🍂 Herbst</h2>
|
|
2
2
|
|
|
3
3
|
<h4 align="center">Herb Syntax Tree</h4>
|
|
4
4
|
|
|
@@ -10,13 +10,13 @@ Herbst (German for "autumn") provides tooling to translate various HTML templati
|
|
|
10
10
|
|
|
11
11
|
## Why Herbst?
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Herb's tooling (the linter, formatter, language server and `Herb::Engine`) works on Herb's syntax tree, which Herb builds from HTML+ERB. Herbst builds the same tree from other templating languages, so you can:
|
|
14
14
|
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
15
|
+
- **Convert** Haml and Slim templates to ERB, one file or a whole `app/views` at a time
|
|
16
|
+
- **Compile** them with `Herb::Engine`, by converting them to ERB first
|
|
17
|
+
- **Use** Herb's HTML-aware tooling on templates that aren't written in ERB
|
|
18
18
|
|
|
19
|
-
All these
|
|
19
|
+
All of these languages render HTML in the end, so they can all share the same syntax tree.
|
|
20
20
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
@@ -34,7 +34,41 @@ gem install herbst
|
|
|
34
34
|
|
|
35
35
|
## Usage
|
|
36
36
|
|
|
37
|
-
Herbst is currently in early development
|
|
37
|
+
Herbst is currently in early development and supports converting Haml and Slim to ERB. Add the `haml` or `slim` gem for the languages you convert, since Herbst doesn't depend on either.
|
|
38
|
+
|
|
39
|
+
Converting and printing ERB is pure Ruby. Formatting the output (`format: true`, and the default for the CLI) runs the Herb formatter through `npx`, so it requires Node.js.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
require "herbst"
|
|
43
|
+
|
|
44
|
+
Herbst::Haml.parse("%p= greeting") # => Herb::AST::DocumentNode
|
|
45
|
+
Herbst::Haml.to_erb("%p= greeting") # => "<p><%= greeting %></p>\n"
|
|
46
|
+
Herbst::Haml.to_erb(source, format: true)
|
|
47
|
+
|
|
48
|
+
Herbst::Slim.parse("p = greeting") # => Herb::AST::DocumentNode
|
|
49
|
+
Herbst::Slim.to_erb("p = greeting") # => "<p><%= greeting %></p>"
|
|
50
|
+
|
|
51
|
+
Herbst::IdentityPrinter.print(Herb.parse(erb, track_whitespace: true).value)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Herbst doesn't compile templates. To compile Haml or Slim, compile its ERB with `Herb::Engine`:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Herb::Engine.new(Herbst::Haml.to_erb(source), escape: true).src
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
From the command line:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
herbst convert app/views/posts/show.html.haml # prints the ERB
|
|
64
|
+
herbst convert app/views # writes show.html.erb next to show.html.haml or .slim
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A single file is printed unless you pass `--write`. Directories, and several files at once, are written next to their sources with the `.haml` or `.slim` extension replaced by `.erb`, so `show.html.haml` becomes `show.html.erb`. A file without a format gets `html`, so `show.haml` becomes `show.html.erb`. Pass `--default-format FORMAT` for another format, or `--no-default-format` to write `show.erb`. The Haml and Slim files are kept, and existing ERB files are skipped unless you pass `--force`. Pass `--no-format` to skip the Herb formatter.
|
|
68
|
+
|
|
69
|
+
Constructs that cannot be converted exactly (for example splatted attribute hashes, which become `tag.attributes(...)`) are reported as warnings. Files that can't be converted, for example because they use an unsupported filter such as `:markdown`, are reported and the remaining files are still converted.
|
|
70
|
+
|
|
71
|
+
The CLI also checks that the ERB parses back into exactly the tree Herbst built, and reports any difference. Pass `--no-verify` to skip the check. The same check is available as `Herbst::RoundTrip.difference(document, erb)`. The CLI exits with status 1 when a file fails to convert or the check finds a difference.
|
|
38
72
|
|
|
39
73
|
## Development
|
|
40
74
|
|
data/Rakefile
CHANGED
|
@@ -7,4 +7,14 @@ require "rubocop/rake_task"
|
|
|
7
7
|
Minitest::TestTask.create
|
|
8
8
|
RuboCop::RakeTask.new
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
desc "Generate RBS signatures from the inline annotations"
|
|
11
|
+
task :rbs do
|
|
12
|
+
sh "bundle exec rbs-inline --opt-out --output=sig/ lib/"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Type check with Steep"
|
|
16
|
+
task :steep do
|
|
17
|
+
sh "bundle exec steep check"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
task default: [:test, :rubocop, :steep]
|
data/exe/herbst
ADDED
data/herbst.gemspec
CHANGED
|
@@ -11,11 +11,12 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.summary = "Herb Syntax Tree - Unified syntax tree for HTML templating languages"
|
|
12
12
|
spec.description = "Herbst provides tooling to translate Haml, Slim, and other HTML templating languages into the unified syntax tree used by Herb, enabling HTML-aware tooling across different HTML templating languages."
|
|
13
13
|
spec.homepage = "https://github.com/marcoroth/herbst"
|
|
14
|
+
spec.license = "MIT"
|
|
14
15
|
spec.required_ruby_version = ">= 3.2.0"
|
|
15
16
|
|
|
16
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
18
|
spec.metadata["source_code_uri"] = "https://github.com/marcoroth/herbst"
|
|
18
|
-
spec.metadata["changelog_uri"] = "https://github.com/marcoroth/herbst/
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/marcoroth/herbst/releases"
|
|
19
20
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
20
21
|
|
|
21
22
|
spec.files = Dir[
|
|
@@ -24,7 +25,8 @@ Gem::Specification.new do |spec|
|
|
|
24
25
|
"Rakefile",
|
|
25
26
|
"README.md",
|
|
26
27
|
"lib/**/*.rb",
|
|
27
|
-
"sig
|
|
28
|
+
"sig/herbst.rbs",
|
|
29
|
+
"sig/herbst/**/*.rbs",
|
|
28
30
|
"exe/*"
|
|
29
31
|
]
|
|
30
32
|
|
|
@@ -32,5 +34,5 @@ Gem::Specification.new do |spec|
|
|
|
32
34
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
33
35
|
spec.require_paths = ["lib"]
|
|
34
36
|
|
|
35
|
-
spec.add_dependency "herb"
|
|
37
|
+
spec.add_dependency "herb", ">= 0.11.1"
|
|
36
38
|
end
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "herb"
|
|
4
|
+
require "herb/html/util"
|
|
5
|
+
require "prism"
|
|
6
|
+
|
|
7
|
+
module Herbst
|
|
8
|
+
module Builder
|
|
9
|
+
#: (String, String) -> Herb::Token
|
|
10
|
+
def token(type, value)
|
|
11
|
+
Herb::Token.from(type, value)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
#: (Array[Herb::AST::Node]) -> Herb::AST::DocumentNode
|
|
15
|
+
def document(children)
|
|
16
|
+
Herb::AST::DocumentNode.build(children: children)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#: (String) -> Herb::AST::HTMLTextNode
|
|
20
|
+
def text(content)
|
|
21
|
+
Herb::AST::HTMLTextNode.build(content: +content)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
#: (String) -> Herb::AST::LiteralNode
|
|
25
|
+
def literal(content)
|
|
26
|
+
Herb::AST::LiteralNode.build(content: +content)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#: (?String) -> Herb::AST::WhitespaceNode
|
|
30
|
+
def whitespace(value = " ")
|
|
31
|
+
Herb::AST::WhitespaceNode.build(value: token("TOKEN_WHITESPACE", value))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#: (String, ?opening: String) -> (Herb::AST::ERBContentNode | Herb::AST::ERBYieldNode)
|
|
35
|
+
def erb(code, opening: "<%=")
|
|
36
|
+
return Herb::AST::ERBYieldNode.build(**erb_tag(opening, code)) if yield?(code)
|
|
37
|
+
|
|
38
|
+
Herb::AST::ERBContentNode.build(**erb_tag(opening, code), parsed: true, valid: Prism.parse(code).errors.empty?)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#: (String) -> bool
|
|
42
|
+
def yield?(code)
|
|
43
|
+
code.include?("yield") && !Prism.parse(code).value.breadth_first_search { |node| node.is_a?(Prism::YieldNode) }.nil?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#: (String) -> Herb::AST::ERBCommentNode
|
|
47
|
+
def erb_comment(content)
|
|
48
|
+
Herb::AST::ERBCommentNode.build(**erb_tag("<%#", content))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#: () -> Herb::AST::ERBEndNode
|
|
52
|
+
def erb_end
|
|
53
|
+
Herb::AST::ERBEndNode.build(**erb_tag("<%", "end"))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: (String, Array[Herb::AST::Node], ?subsequent: (Herb::AST::ERBIfNode | Herb::AST::ERBElseNode)?, ?end_node: Herb::AST::ERBEndNode?) -> Herb::AST::ERBIfNode
|
|
57
|
+
def erb_if(code, statements, subsequent: nil, end_node: erb_end)
|
|
58
|
+
Herb::AST::ERBIfNode.build(**erb_tag("<%", code), statements: statements, subsequent: subsequent, end_node: end_node)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#: (String, Array[Herb::AST::Node], ?else_clause: Herb::AST::ERBElseNode?) -> Herb::AST::ERBUnlessNode
|
|
62
|
+
def erb_unless(code, statements, else_clause: nil)
|
|
63
|
+
Herb::AST::ERBUnlessNode.build(**erb_tag("<%", code), statements: statements, else_clause: else_clause, end_node: erb_end)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#: (Array[Herb::AST::Node]) -> Herb::AST::ERBElseNode
|
|
67
|
+
def erb_else(statements)
|
|
68
|
+
Herb::AST::ERBElseNode.build(**erb_tag("<%", "else"), statements: statements)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#: (String, Array[Herb::AST::ERBWhenNode], ?else_clause: Herb::AST::ERBElseNode?) -> Herb::AST::ERBCaseNode
|
|
72
|
+
def erb_case(code, conditions, else_clause: nil)
|
|
73
|
+
Herb::AST::ERBCaseNode.build(**erb_tag("<%", code), conditions: conditions, else_clause: else_clause, end_node: erb_end)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
#: (String, Array[Herb::AST::Node]) -> Herb::AST::ERBWhenNode
|
|
77
|
+
def erb_when(code, statements)
|
|
78
|
+
Herb::AST::ERBWhenNode.build(**erb_tag("<%", code), statements: statements)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#: (String, Array[Herb::AST::Node], ?rescue_clause: Herb::AST::ERBRescueNode?, ?else_clause: Herb::AST::ERBElseNode?, ?ensure_clause: Herb::AST::ERBEnsureNode?) -> Herb::AST::ERBBeginNode
|
|
82
|
+
def erb_begin(code, statements, rescue_clause: nil, else_clause: nil, ensure_clause: nil)
|
|
83
|
+
Herb::AST::ERBBeginNode.build(
|
|
84
|
+
**erb_tag("<%", code),
|
|
85
|
+
statements: statements,
|
|
86
|
+
rescue_clause: rescue_clause,
|
|
87
|
+
else_clause: else_clause,
|
|
88
|
+
ensure_clause: ensure_clause,
|
|
89
|
+
end_node: erb_end
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#: (String, Array[Herb::AST::Node], ?subsequent: Herb::AST::ERBRescueNode?) -> Herb::AST::ERBRescueNode
|
|
94
|
+
def erb_rescue(code, statements, subsequent: nil)
|
|
95
|
+
Herb::AST::ERBRescueNode.build(**erb_tag("<%", code), statements: statements, subsequent: subsequent)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#: (Array[Herb::AST::Node]) -> Herb::AST::ERBEnsureNode
|
|
99
|
+
def erb_ensure(statements)
|
|
100
|
+
Herb::AST::ERBEnsureNode.build(**erb_tag("<%", "ensure"), statements: statements)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#: (String, Array[Herb::AST::Node]) -> Herb::AST::ERBWhileNode
|
|
104
|
+
def erb_while(code, statements)
|
|
105
|
+
Herb::AST::ERBWhileNode.build(**erb_tag("<%", code), statements: statements, end_node: erb_end)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#: (String, Array[Herb::AST::Node]) -> Herb::AST::ERBUntilNode
|
|
109
|
+
def erb_until(code, statements)
|
|
110
|
+
Herb::AST::ERBUntilNode.build(**erb_tag("<%", code), statements: statements, end_node: erb_end)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#: (String, Array[Herb::AST::Node]) -> Herb::AST::ERBForNode
|
|
114
|
+
def erb_for(code, statements)
|
|
115
|
+
Herb::AST::ERBForNode.build(**erb_tag("<%", code), statements: statements, end_node: erb_end)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#: (String, Array[Herb::AST::Node], ?opening: String) -> Herb::AST::ERBBlockNode
|
|
119
|
+
def erb_block(code, body, opening: "<%")
|
|
120
|
+
Herb::AST::ERBBlockNode.build(**erb_tag(opening, code), body: body, block_arguments: block_arguments(code), end_node: erb_end)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#: (String) -> Array[Herb::AST::RubyParameterNode]
|
|
124
|
+
def block_arguments(code)
|
|
125
|
+
# Like Herb, only a block passed to the first statement's method call has parameters, so
|
|
126
|
+
# `items.each do |item|` has them and `list = items.map do |item|` doesn't.
|
|
127
|
+
call = Prism.parse("#{code}\nend").value.statements.body.first
|
|
128
|
+
return [] unless call.is_a?(Prism::CallNode)
|
|
129
|
+
|
|
130
|
+
block = call.block
|
|
131
|
+
return [] unless block.is_a?(Prism::BlockNode)
|
|
132
|
+
|
|
133
|
+
block_parameters = block.parameters
|
|
134
|
+
return [] unless block_parameters.is_a?(Prism::BlockParametersNode)
|
|
135
|
+
|
|
136
|
+
parameters = block_parameters.parameters
|
|
137
|
+
return [] unless parameters
|
|
138
|
+
|
|
139
|
+
all = [*parameters.requireds, *parameters.optionals, parameters.rest, *parameters.posts, *parameters.keywords, parameters.keyword_rest, parameters.block].compact
|
|
140
|
+
all = all.flat_map { |parameter| destructured(parameter) }
|
|
141
|
+
|
|
142
|
+
all.filter_map { |parameter| ruby_parameter(parameter) }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
#: (Prism::node) -> Herb::AST::RubyParameterNode?
|
|
146
|
+
def ruby_parameter(parameter)
|
|
147
|
+
case parameter
|
|
148
|
+
when Prism::RequiredParameterNode
|
|
149
|
+
ruby_parameter_node(parameter.name, "positional", true)
|
|
150
|
+
when Prism::OptionalParameterNode
|
|
151
|
+
ruby_parameter_node(parameter.name, "positional", false, parameter.value)
|
|
152
|
+
when Prism::RestParameterNode
|
|
153
|
+
ruby_parameter_node(parameter.name, "rest", false)
|
|
154
|
+
when Prism::RequiredKeywordParameterNode
|
|
155
|
+
ruby_parameter_node(parameter.name, "keyword", true)
|
|
156
|
+
when Prism::OptionalKeywordParameterNode
|
|
157
|
+
ruby_parameter_node(parameter.name, "keyword", false, parameter.value)
|
|
158
|
+
when Prism::KeywordRestParameterNode
|
|
159
|
+
ruby_parameter_node(parameter.name, "keyword_rest", false)
|
|
160
|
+
when Prism::BlockParameterNode
|
|
161
|
+
ruby_parameter_node(parameter.name, "block", false)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
#: (Symbol?, String, bool, ?Prism::node?) -> Herb::AST::RubyParameterNode?
|
|
166
|
+
def ruby_parameter_node(name, kind, required, value = nil)
|
|
167
|
+
return unless name
|
|
168
|
+
|
|
169
|
+
default = Herb::AST::RubyLiteralNode.build(content: +value.slice) if value
|
|
170
|
+
|
|
171
|
+
Herb::AST::RubyParameterNode.build(
|
|
172
|
+
name: token("TOKEN_IDENTIFIER", name.to_s), default_value: default, kind: +kind, required: required
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
#: (String, ?attributes: Array[Herb::AST::Node], ?body: Array[Herb::AST::Node], ?self_closing: bool) -> Herb::AST::HTMLElementNode
|
|
177
|
+
def element(name, attributes: [], body: [], self_closing: false)
|
|
178
|
+
void = Herb::HTML::Util.void_element?(name)
|
|
179
|
+
|
|
180
|
+
self_closing &&= !void
|
|
181
|
+
|
|
182
|
+
children = open_tag_children(attributes)
|
|
183
|
+
children << whitespace if self_closing
|
|
184
|
+
|
|
185
|
+
open_tag = Herb::AST::HTMLOpenTagNode.build(
|
|
186
|
+
tag_opening: token("TOKEN_HTML_TAG_START", "<"),
|
|
187
|
+
tag_name: token("TOKEN_IDENTIFIER", name),
|
|
188
|
+
tag_closing: self_closing ? token("TOKEN_HTML_TAG_SELF_CLOSE", "/>") : token("TOKEN_HTML_TAG_END", ">"),
|
|
189
|
+
children: children,
|
|
190
|
+
is_void: self_closing
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if Herb::HTML::Util.raw_text_element?(name)
|
|
194
|
+
body = merge_literals(body.map do |node|
|
|
195
|
+
node.is_a?(Herb::AST::HTMLTextNode) ? literal(node.content.to_s) : node
|
|
196
|
+
end)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Herb reads the content of elements like `<script>` and `<textarea>` as text and ERB, so Herb
|
|
200
|
+
# builds one that contains anything else, like the templates in `<script type="text/ng-template">`.
|
|
201
|
+
if Herb::HTML::Util.raw_text_element?(name) || Herb::HTML::Util.rcdata_element?(name)
|
|
202
|
+
text_only = body.all? do |node|
|
|
203
|
+
node.is_a?(Herb::AST::LiteralNode) || node.is_a?(Herb::AST::HTMLTextNode) || node.is_a?(Herb::AST::ERBContentNode)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
unless text_only
|
|
207
|
+
source = IdentityPrinter.print(element(name, attributes: attributes, body: []).tap { |node| node.body.replace(body) })
|
|
208
|
+
|
|
209
|
+
return Herb.parse(source, track_whitespace: true).value.children.fetch(0) #: Herb::AST::HTMLElementNode
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
close_tag = unless void || self_closing
|
|
214
|
+
Herb::AST::HTMLCloseTagNode.build(
|
|
215
|
+
tag_opening: token("TOKEN_HTML_TAG_START_CLOSE", "</"),
|
|
216
|
+
tag_name: token("TOKEN_IDENTIFIER", name),
|
|
217
|
+
tag_closing: token("TOKEN_HTML_TAG_END", ">")
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
Herb::AST::HTMLElementNode.build(
|
|
222
|
+
open_tag: open_tag,
|
|
223
|
+
tag_name: token("TOKEN_IDENTIFIER", name),
|
|
224
|
+
body: body,
|
|
225
|
+
close_tag: close_tag,
|
|
226
|
+
is_void: void || self_closing,
|
|
227
|
+
element_source: "HTML"
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
#: (String | Herb::AST::ERBContentNode, ?Array[Herb::AST::Node]?) -> Herb::AST::HTMLAttributeNode
|
|
232
|
+
def attribute(name, value = nil)
|
|
233
|
+
name_node = Herb::AST::HTMLAttributeNameNode.build(children: [name.is_a?(String) ? literal(name) : name])
|
|
234
|
+
return Herb::AST::HTMLAttributeNode.build(name: name_node) if value.nil?
|
|
235
|
+
|
|
236
|
+
Herb::AST::HTMLAttributeNode.build(
|
|
237
|
+
name: name_node,
|
|
238
|
+
equals: token("TOKEN_EQUALS", "="),
|
|
239
|
+
value: Herb::AST::HTMLAttributeValueNode.build(
|
|
240
|
+
open_quote: token("TOKEN_QUOTE", '"'),
|
|
241
|
+
children: merge_literals(value),
|
|
242
|
+
close_quote: token("TOKEN_QUOTE", '"'),
|
|
243
|
+
quoted: true
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
#: (Array[Herb::AST::Node]) -> Herb::AST::HTMLCommentNode
|
|
249
|
+
def comment(children)
|
|
250
|
+
children = children.map { |node| node.is_a?(Herb::AST::HTMLTextNode) ? literal(node.content.to_s) : node }
|
|
251
|
+
|
|
252
|
+
# Herb doesn't parse elements inside a comment, so Herb builds a comment that contains any.
|
|
253
|
+
unless children.all? { |node| node.is_a?(Herb::AST::LiteralNode) || node.is_a?(Herb::AST::ERBContentNode) }
|
|
254
|
+
source = "<!--#{children.map { |node| IdentityPrinter.print(node) }.join}-->"
|
|
255
|
+
|
|
256
|
+
return Herb.parse(source, track_whitespace: true).value.children.fetch(0) #: Herb::AST::HTMLCommentNode
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
Herb::AST::HTMLCommentNode.build(
|
|
260
|
+
comment_start: token("TOKEN_HTML_COMMENT_START", "<!--"),
|
|
261
|
+
children: merge_literals(children),
|
|
262
|
+
comment_end: token("TOKEN_HTML_COMMENT_END", "-->")
|
|
263
|
+
)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
#: (String) -> Herb::AST::HTMLDoctypeNode
|
|
267
|
+
def doctype(content)
|
|
268
|
+
Herb::AST::HTMLDoctypeNode.build(
|
|
269
|
+
tag_opening: token("TOKEN_HTML_DOCTYPE", "<!DOCTYPE"),
|
|
270
|
+
children: [literal(content)],
|
|
271
|
+
tag_closing: token("TOKEN_HTML_TAG_END", ">")
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
#: (String) -> Herb::AST::XMLDeclarationNode
|
|
276
|
+
def xml_declaration(content)
|
|
277
|
+
Herb::AST::XMLDeclarationNode.build(
|
|
278
|
+
tag_opening: token("TOKEN_XML_DECLARATION", "<?xml"),
|
|
279
|
+
children: [literal(content)],
|
|
280
|
+
tag_closing: token("TOKEN_XML_DECLARATION_END", "?>")
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
#: (Prism::node) -> Array[Prism::node]
|
|
285
|
+
def destructured(parameter)
|
|
286
|
+
return [parameter] unless parameter.is_a?(Prism::MultiTargetNode)
|
|
287
|
+
|
|
288
|
+
[*parameter.lefts, *parameter.rights].flat_map { |target| destructured(target) }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
#: (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
292
|
+
def open_tag_children(attributes)
|
|
293
|
+
attributes.each_with_index.flat_map do |node, index|
|
|
294
|
+
# Herb reads ERB in an open tag as an attribute name when an attribute with a value follows it.
|
|
295
|
+
valued = attributes.drop(index + 1).any? { |following| following.is_a?(Herb::AST::HTMLAttributeNode) && following.equals }
|
|
296
|
+
|
|
297
|
+
if node.is_a?(Herb::AST::ERBContentNode) && valued
|
|
298
|
+
[whitespace, attribute(node)]
|
|
299
|
+
else
|
|
300
|
+
[whitespace, node]
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
#: [T < Herb::AST::Node] (T) -> T
|
|
306
|
+
def merge_texts!(node)
|
|
307
|
+
node.instance_variables.each do |name|
|
|
308
|
+
value = node.instance_variable_get(name)
|
|
309
|
+
|
|
310
|
+
if value.is_a?(Array)
|
|
311
|
+
value.each { |child| merge_texts!(child) if child.is_a?(Herb::AST::Node) }
|
|
312
|
+
value.replace(merge_adjacent_texts(value))
|
|
313
|
+
elsif value.is_a?(Herb::AST::Node)
|
|
314
|
+
merge_texts!(value)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
node
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
#: (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
322
|
+
def merge_adjacent_texts(nodes)
|
|
323
|
+
nodes.each_with_object([]) do |node, merged|
|
|
324
|
+
last = merged.last
|
|
325
|
+
|
|
326
|
+
if node.is_a?(Herb::AST::HTMLTextNode) && last.is_a?(Herb::AST::HTMLTextNode)
|
|
327
|
+
merged[-1] = text(last.content.to_s + node.content.to_s)
|
|
328
|
+
else
|
|
329
|
+
merged << node
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
#: (Array[Herb::AST::Node]) -> Array[Herb::AST::Node]
|
|
335
|
+
def merge_literals(nodes)
|
|
336
|
+
nodes.each_with_object([]) do |node, merged|
|
|
337
|
+
next if node.is_a?(Herb::AST::LiteralNode) && node.content.to_s.empty?
|
|
338
|
+
|
|
339
|
+
last = merged.last
|
|
340
|
+
|
|
341
|
+
if node.is_a?(Herb::AST::LiteralNode) && last.is_a?(Herb::AST::LiteralNode)
|
|
342
|
+
merged[-1] = literal(last.content.to_s + node.content.to_s)
|
|
343
|
+
else
|
|
344
|
+
merged << node
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
#: (String, String) -> { tag_opening: Herb::Token, content: Herb::Token, tag_closing: Herb::Token }
|
|
350
|
+
def erb_tag(opening, code)
|
|
351
|
+
{
|
|
352
|
+
tag_opening: token("TOKEN_ERB_START", opening),
|
|
353
|
+
content: token("TOKEN_ERB_CONTENT", " #{code.strip} "),
|
|
354
|
+
tag_closing: token("TOKEN_ERB_END", "%>"),
|
|
355
|
+
}
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|