grsx 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 +7 -0
- data/.github/workflows/main.yml +20 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Appraisals +17 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +8 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +274 -0
- data/Guardfile +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +437 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/test +43 -0
- data/docker-compose.yml +29 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_6_1.gemfile +8 -0
- data/gemfiles/rails_6_1.gemfile.lock +260 -0
- data/gemfiles/rails_7_0.gemfile +7 -0
- data/gemfiles/rails_7_0.gemfile.lock +265 -0
- data/gemfiles/rails_7_1.gemfile +7 -0
- data/gemfiles/rails_7_1.gemfile.lock +295 -0
- data/gemfiles/rails_7_2.gemfile +7 -0
- data/gemfiles/rails_7_2.gemfile.lock +290 -0
- data/gemfiles/rails_8_0.gemfile +8 -0
- data/gemfiles/rails_8_0.gemfile.lock +344 -0
- data/gemfiles/rails_8_1.gemfile +8 -0
- data/gemfiles/rails_8_1.gemfile.lock +313 -0
- data/gemfiles/rails_master.gemfile +7 -0
- data/gemfiles/rails_master.gemfile.lock +296 -0
- data/grsx.gemspec +43 -0
- data/lib/generators/grsx/phlex_component/phlex_component_generator.rb +40 -0
- data/lib/generators/grsx/phlex_component/templates/component.rb.tt +12 -0
- data/lib/generators/grsx/phlex_component/templates/component.rbx.tt +6 -0
- data/lib/grsx/component_resolver.rb +64 -0
- data/lib/grsx/configuration.rb +14 -0
- data/lib/grsx/lexer.rb +325 -0
- data/lib/grsx/nodes/abstract_attr.rb +12 -0
- data/lib/grsx/nodes/abstract_element.rb +13 -0
- data/lib/grsx/nodes/abstract_node.rb +31 -0
- data/lib/grsx/nodes/component_element.rb +69 -0
- data/lib/grsx/nodes/component_prop.rb +29 -0
- data/lib/grsx/nodes/declaration.rb +15 -0
- data/lib/grsx/nodes/expression.rb +15 -0
- data/lib/grsx/nodes/expression_group.rb +15 -0
- data/lib/grsx/nodes/fragment.rb +30 -0
- data/lib/grsx/nodes/html_attr.rb +13 -0
- data/lib/grsx/nodes/html_element.rb +49 -0
- data/lib/grsx/nodes/newline.rb +9 -0
- data/lib/grsx/nodes/raw.rb +23 -0
- data/lib/grsx/nodes/root.rb +19 -0
- data/lib/grsx/nodes/text.rb +15 -0
- data/lib/grsx/nodes/util.rb +9 -0
- data/lib/grsx/nodes.rb +20 -0
- data/lib/grsx/parser.rb +238 -0
- data/lib/grsx/phlex_compiler.rb +223 -0
- data/lib/grsx/phlex_component.rb +361 -0
- data/lib/grsx/phlex_runtime.rb +70 -0
- data/lib/grsx/prop_inspector.rb +52 -0
- data/lib/grsx/rails/engine.rb +24 -0
- data/lib/grsx/rails/phlex_reloader.rb +25 -0
- data/lib/grsx/template.rb +12 -0
- data/lib/grsx/version.rb +3 -0
- data/lib/grsx.rb +35 -0
- metadata +324 -0
data/lib/grsx/lexer.rb
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
class Lexer
|
|
3
|
+
class SyntaxError < StandardError
|
|
4
|
+
attr_reader :line, :path
|
|
5
|
+
|
|
6
|
+
def initialize(lexer)
|
|
7
|
+
@line = lexer.current_line
|
|
8
|
+
# Template#identifier is the file path; Anonymous is an empty String.
|
|
9
|
+
ident = lexer.template.identifier
|
|
10
|
+
@path = ident.to_s.empty? ? nil : ident.to_s
|
|
11
|
+
|
|
12
|
+
location = path ? "#{File.basename(path)}:#{line}" : "line #{line}"
|
|
13
|
+
snippet = lexer.scanner.peek(30).chomp.inspect
|
|
14
|
+
|
|
15
|
+
super("#{location}: unexpected token near #{snippet}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
PATTERNS = {
|
|
20
|
+
open_expression: /{/,
|
|
21
|
+
close_expression: /}/,
|
|
22
|
+
expression_content: /[^}{"'<]+/,
|
|
23
|
+
open_tag_def: /<(?!\/)/,
|
|
24
|
+
open_tag_end: /<\//,
|
|
25
|
+
close_tag: /\s*\/?>/ ,
|
|
26
|
+
close_self_closing_tag: /\s*\/>/,
|
|
27
|
+
tag_name: /\/?[A-Za-z0-9\-_.]+/,
|
|
28
|
+
text_content: /[^<{#]+/,
|
|
29
|
+
comment: /^\p{Blank}*#.*(\n|\z)/,
|
|
30
|
+
whitespace: /\s+/,
|
|
31
|
+
attr: /[A-Za-z0-9\-_\.:]+/,
|
|
32
|
+
open_attr_splat: /{\*\*/,
|
|
33
|
+
attr_assignment: /=/,
|
|
34
|
+
double_quote: /"/,
|
|
35
|
+
single_quote: /'/,
|
|
36
|
+
double_quoted_text_content: /[^"]+/,
|
|
37
|
+
single_quoted_text_content: /[^']+/,
|
|
38
|
+
expression_internal_tag_prefixes: /(\s+(&&|\|\||\?|:|do|do\s*\|[^\|]+\||{|{\s*\|[^\|]+\|)\s+\z|\A\s*\z)/,
|
|
39
|
+
declaration: /<![^>]*>/
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
attr_reader :stack, :tokens, :scanner, :element_resolver, :template
|
|
43
|
+
attr_accessor :curr_expr, :curr_default_text, :curr_quoted_text
|
|
44
|
+
|
|
45
|
+
def initialize(template, element_resolver)
|
|
46
|
+
@template = template
|
|
47
|
+
@scanner = StringScanner.new(template.source)
|
|
48
|
+
@element_resolver = element_resolver
|
|
49
|
+
@stack = [:default]
|
|
50
|
+
@curr_expr = ""
|
|
51
|
+
@curr_default_text = ""
|
|
52
|
+
@curr_quoted_text = ""
|
|
53
|
+
@tokens = []
|
|
54
|
+
@line = 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Current line number (1-indexed). Derived from the scanner's current
|
|
58
|
+
# position — counts newlines consumed so far. Called only on errors
|
|
59
|
+
# so O(n) is acceptable.
|
|
60
|
+
def current_line
|
|
61
|
+
scanner.string[0, scanner.pos].count("\n") + 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tokenize
|
|
65
|
+
until scanner.eos?
|
|
66
|
+
case stack.last
|
|
67
|
+
when :default
|
|
68
|
+
if scanner.scan(PATTERNS[:declaration])
|
|
69
|
+
tokens << [:DECLARATION, scanner.matched]
|
|
70
|
+
elsif scanner.scan(PATTERNS[:open_tag_def])
|
|
71
|
+
open_tag_def
|
|
72
|
+
elsif scanner.scan(PATTERNS[:open_expression])
|
|
73
|
+
open_expression
|
|
74
|
+
elsif scanner.scan(PATTERNS[:comment])
|
|
75
|
+
tokens << [:NEWLINE]
|
|
76
|
+
elsif scanner.check(PATTERNS[:text_content])
|
|
77
|
+
stack.push(:default_text)
|
|
78
|
+
else
|
|
79
|
+
raise SyntaxError, self
|
|
80
|
+
end
|
|
81
|
+
when :tag
|
|
82
|
+
if scanner.scan(PATTERNS[:open_tag_def])
|
|
83
|
+
open_tag_def
|
|
84
|
+
elsif scanner.scan(PATTERNS[:open_tag_end])
|
|
85
|
+
# Check if this is </> (fragment close) vs </tagname>
|
|
86
|
+
if scanner.check(/>\s*/)
|
|
87
|
+
scanner.scan(/>\s*/)
|
|
88
|
+
tokens << [:CLOSE_FRAGMENT]
|
|
89
|
+
stack.pop
|
|
90
|
+
else
|
|
91
|
+
tokens << [:OPEN_TAG_END]
|
|
92
|
+
stack.push(:tag_end)
|
|
93
|
+
end
|
|
94
|
+
elsif scanner.scan(PATTERNS[:open_expression])
|
|
95
|
+
open_expression
|
|
96
|
+
elsif scanner.scan(PATTERNS[:comment])
|
|
97
|
+
tokens << [:NEWLINE]
|
|
98
|
+
elsif scanner.check(PATTERNS[:text_content])
|
|
99
|
+
stack.push(:default_text)
|
|
100
|
+
else
|
|
101
|
+
raise SyntaxError, self
|
|
102
|
+
end
|
|
103
|
+
when :default_text
|
|
104
|
+
if scanner.scan(PATTERNS[:text_content])
|
|
105
|
+
self.curr_default_text << scanner.matched
|
|
106
|
+
if scanner.matched.end_with?('\\') && scanner.peek(1) == "{"
|
|
107
|
+
self.curr_default_text << scanner.getch
|
|
108
|
+
elsif scanner.matched.end_with?('\\') && scanner.peek(1) == "#"
|
|
109
|
+
self.curr_default_text << scanner.getch
|
|
110
|
+
else
|
|
111
|
+
if scanner.peek(1) == "#"
|
|
112
|
+
# If the next token is a comment, trim trailing whitespace from
|
|
113
|
+
# the text value so we don't add to the indentation of the next
|
|
114
|
+
# value that is output after the comment
|
|
115
|
+
self.curr_default_text = curr_default_text.gsub(/^\p{Blank}*\z/, "")
|
|
116
|
+
end
|
|
117
|
+
tokens << [:TEXT, curr_default_text]
|
|
118
|
+
self.curr_default_text = ""
|
|
119
|
+
stack.pop
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
raise SyntaxError, self
|
|
123
|
+
end
|
|
124
|
+
when :expression
|
|
125
|
+
if scanner.scan(PATTERNS[:close_expression])
|
|
126
|
+
tokens << [:EXPRESSION_BODY, curr_expr]
|
|
127
|
+
tokens << [:CLOSE_EXPRESSION]
|
|
128
|
+
self.curr_expr = ""
|
|
129
|
+
stack.pop
|
|
130
|
+
elsif scanner.scan(PATTERNS[:open_expression])
|
|
131
|
+
expression_inner_bracket
|
|
132
|
+
elsif scanner.scan(PATTERNS[:double_quote])
|
|
133
|
+
expression_inner_double_quote
|
|
134
|
+
elsif scanner.scan(PATTERNS[:single_quote])
|
|
135
|
+
expression_inner_single_quote
|
|
136
|
+
elsif scanner.scan(PATTERNS[:open_tag_def])
|
|
137
|
+
potential_expression_inner_tag
|
|
138
|
+
elsif expression_content?
|
|
139
|
+
self.curr_expr << scanner.matched
|
|
140
|
+
else
|
|
141
|
+
raise SyntaxError, self
|
|
142
|
+
end
|
|
143
|
+
when :expression_inner_bracket
|
|
144
|
+
if scanner.scan(PATTERNS[:close_expression])
|
|
145
|
+
self.curr_expr << scanner.matched
|
|
146
|
+
stack.pop
|
|
147
|
+
elsif scanner.scan(PATTERNS[:open_expression])
|
|
148
|
+
expression_inner_bracket
|
|
149
|
+
elsif scanner.scan(PATTERNS[:double_quote])
|
|
150
|
+
expression_inner_double_quote
|
|
151
|
+
elsif scanner.scan(PATTERNS[:single_quote])
|
|
152
|
+
expression_inner_single_quote
|
|
153
|
+
elsif scanner.scan(PATTERNS[:open_tag_def])
|
|
154
|
+
potential_expression_inner_tag
|
|
155
|
+
elsif expression_content?
|
|
156
|
+
self.curr_expr << scanner.matched
|
|
157
|
+
else
|
|
158
|
+
raise SyntaxError, self
|
|
159
|
+
end
|
|
160
|
+
when :expression_inner_double_quote
|
|
161
|
+
if scanner.check(PATTERNS[:double_quote])
|
|
162
|
+
expression_quoted_string_content
|
|
163
|
+
elsif scanner.scan(PATTERNS[:double_quoted_text_content])
|
|
164
|
+
self.curr_expr << scanner.matched
|
|
165
|
+
else
|
|
166
|
+
raise SyntaxError, self
|
|
167
|
+
end
|
|
168
|
+
when :expression_inner_single_quote
|
|
169
|
+
if scanner.check(PATTERNS[:single_quote])
|
|
170
|
+
expression_quoted_string_content
|
|
171
|
+
elsif scanner.scan(PATTERNS[:single_quoted_text_content])
|
|
172
|
+
self.curr_expr << scanner.matched
|
|
173
|
+
else
|
|
174
|
+
raise SyntaxError, self
|
|
175
|
+
end
|
|
176
|
+
when :tag_def
|
|
177
|
+
if scanner.scan(PATTERNS[:close_self_closing_tag])
|
|
178
|
+
tokens << [:CLOSE_TAG_DEF]
|
|
179
|
+
tokens << [:OPEN_TAG_END]
|
|
180
|
+
tokens << [:CLOSE_TAG_END]
|
|
181
|
+
stack.pop(2)
|
|
182
|
+
elsif scanner.scan(PATTERNS[:close_tag])
|
|
183
|
+
tokens << [:CLOSE_TAG_DEF]
|
|
184
|
+
stack.pop
|
|
185
|
+
elsif scanner.scan(PATTERNS[:tag_name])
|
|
186
|
+
tokens << [:TAG_DETAILS, tag_details(scanner.matched)]
|
|
187
|
+
elsif scanner.scan(PATTERNS[:whitespace])
|
|
188
|
+
scanner.matched.count("\n").times { tokens << [:NEWLINE] }
|
|
189
|
+
tokens << [:OPEN_ATTRS]
|
|
190
|
+
stack.push(:tag_attrs)
|
|
191
|
+
else
|
|
192
|
+
raise SyntaxError, self
|
|
193
|
+
end
|
|
194
|
+
when :tag_end
|
|
195
|
+
if scanner.scan(PATTERNS[:close_tag])
|
|
196
|
+
tokens << [:CLOSE_TAG_END]
|
|
197
|
+
stack.pop(2)
|
|
198
|
+
elsif scanner.scan(PATTERNS[:tag_name])
|
|
199
|
+
tokens << [:TAG_NAME, scanner.matched]
|
|
200
|
+
else
|
|
201
|
+
raise SyntaxError, self
|
|
202
|
+
end
|
|
203
|
+
when :tag_attrs
|
|
204
|
+
if scanner.scan(PATTERNS[:whitespace])
|
|
205
|
+
scanner.matched.count("\n").times { tokens << [:NEWLINE] }
|
|
206
|
+
elsif scanner.check(PATTERNS[:close_tag])
|
|
207
|
+
tokens << [:CLOSE_ATTRS]
|
|
208
|
+
stack.pop
|
|
209
|
+
elsif scanner.scan(PATTERNS[:attr_assignment])
|
|
210
|
+
tokens << [:OPEN_ATTR_VALUE]
|
|
211
|
+
stack.push(:tag_attr_value)
|
|
212
|
+
elsif scanner.scan(PATTERNS[:attr])
|
|
213
|
+
tokens << [:ATTR_NAME, scanner.matched.strip]
|
|
214
|
+
elsif scanner.scan(PATTERNS[:open_attr_splat])
|
|
215
|
+
tokens << [:OPEN_ATTR_SPLAT]
|
|
216
|
+
tokens << [:OPEN_EXPRESSION]
|
|
217
|
+
stack.push(:tag_attr_splat, :expression)
|
|
218
|
+
else
|
|
219
|
+
raise SyntaxError, self
|
|
220
|
+
end
|
|
221
|
+
when :tag_attr_value
|
|
222
|
+
if scanner.scan(PATTERNS[:double_quote])
|
|
223
|
+
stack.push(:quoted_text)
|
|
224
|
+
elsif scanner.scan(PATTERNS[:open_expression])
|
|
225
|
+
open_expression
|
|
226
|
+
elsif scanner.scan(PATTERNS[:whitespace]) || scanner.check(PATTERNS[:close_tag])
|
|
227
|
+
tokens << [:CLOSE_ATTR_VALUE]
|
|
228
|
+
scanner.matched.count("\n").times { tokens << [:NEWLINE] }
|
|
229
|
+
stack.pop
|
|
230
|
+
else
|
|
231
|
+
raise SyntaxError, self
|
|
232
|
+
end
|
|
233
|
+
when :tag_attr_splat
|
|
234
|
+
# Splat is consumed by :expression. It pops control back to here once
|
|
235
|
+
# it's done, and we just record the completion and pop back to :tag_attrs
|
|
236
|
+
tokens << [:CLOSE_ATTR_SPLAT]
|
|
237
|
+
stack.pop
|
|
238
|
+
when :quoted_text
|
|
239
|
+
if scanner.scan(PATTERNS[:double_quoted_text_content])
|
|
240
|
+
self.curr_quoted_text << scanner.matched
|
|
241
|
+
if scanner.matched.end_with?('\\') && scanner.peek(1) == "\""
|
|
242
|
+
self.curr_quoted_text << scanner.getch
|
|
243
|
+
end
|
|
244
|
+
elsif scanner.scan(PATTERNS[:double_quote])
|
|
245
|
+
tokens << [:TEXT, curr_quoted_text]
|
|
246
|
+
self.curr_quoted_text = ""
|
|
247
|
+
stack.pop
|
|
248
|
+
else
|
|
249
|
+
raise SyntaxError, self
|
|
250
|
+
end
|
|
251
|
+
else
|
|
252
|
+
raise SyntaxError, self
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
tokens
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def potential_expression_inner_tag
|
|
260
|
+
if self.curr_expr =~ PATTERNS[:expression_internal_tag_prefixes]
|
|
261
|
+
tokens << [:EXPRESSION_BODY, curr_expr]
|
|
262
|
+
self.curr_expr = ""
|
|
263
|
+
open_tag_def
|
|
264
|
+
else
|
|
265
|
+
self.curr_expr << scanner.matched
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def open_tag_def
|
|
270
|
+
# Lookahead: if the scanner is immediately at `>` (with optional leading whitespace)
|
|
271
|
+
# then this is a JSX fragment opener `<>` — emit OPEN_FRAGMENT and stay in current context.
|
|
272
|
+
if scanner.check(/\s*>/)
|
|
273
|
+
scanner.scan(/\s*>/)
|
|
274
|
+
tokens << [:OPEN_FRAGMENT]
|
|
275
|
+
stack.push(:tag) # enter :tag context so children are parsed, fragment close handled above
|
|
276
|
+
else
|
|
277
|
+
tokens << [:OPEN_TAG_DEF]
|
|
278
|
+
stack.push(:tag, :tag_def)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def open_expression
|
|
283
|
+
tokens << [:OPEN_EXPRESSION]
|
|
284
|
+
stack.push(:expression)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def expression_inner_bracket
|
|
288
|
+
self.curr_expr << scanner.matched
|
|
289
|
+
stack.push(:expression_inner_bracket)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def expression_inner_double_quote
|
|
293
|
+
self.curr_expr << scanner.matched
|
|
294
|
+
stack.push(:expression_inner_double_quote)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def expression_inner_single_quote
|
|
298
|
+
self.curr_expr << scanner.matched
|
|
299
|
+
stack.push(:expression_inner_single_quote)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def expression_quoted_string_content
|
|
303
|
+
self.curr_expr << scanner.getch
|
|
304
|
+
stack.pop unless curr_expr.end_with?('\\')
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def expression_content?
|
|
308
|
+
# PATTERNS[:expression_content] ends at `<` characters, because we need to
|
|
309
|
+
# separately scan for allowed open_tag_defs within expressions. We should
|
|
310
|
+
# support any found open_tag_ends as expression content, as that means the
|
|
311
|
+
# open_tag_def was not considered allowed (or stack would be inside
|
|
312
|
+
# :tag_def instead of :expression) so we should thus also consider the
|
|
313
|
+
# open_tag_end to just be a part of the expression (maybe its in a string,
|
|
314
|
+
# etc).
|
|
315
|
+
scanner.scan(PATTERNS[:expression_content]) || scanner.scan(PATTERNS[:open_tag_end])
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def tag_details(name)
|
|
319
|
+
type = element_resolver.component?(name, template) ? :component : :html
|
|
320
|
+
details = { name: scanner.matched, type: type }
|
|
321
|
+
details[:component_class] = element_resolver.component_class(name, template) if type == :component
|
|
322
|
+
details
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
module Nodes
|
|
3
|
+
class AbstractNode
|
|
4
|
+
# Compact adjacent Raw/Newline nodes into a single Raw node.
|
|
5
|
+
# Used by the parser when building the AST.
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def compact(nodes)
|
|
9
|
+
compacted = []
|
|
10
|
+
curr_raw = nil
|
|
11
|
+
|
|
12
|
+
nodes.each do |node|
|
|
13
|
+
if node.is_a?(Newline) && curr_raw
|
|
14
|
+
curr_raw.merge(Raw.new("\n"))
|
|
15
|
+
elsif node.is_a?(Raw)
|
|
16
|
+
if !curr_raw
|
|
17
|
+
curr_raw ||= Raw.new("")
|
|
18
|
+
compacted << curr_raw
|
|
19
|
+
end
|
|
20
|
+
curr_raw.merge(node)
|
|
21
|
+
else
|
|
22
|
+
curr_raw = nil
|
|
23
|
+
compacted << node
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
compacted
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
module Nodes
|
|
3
|
+
class ComponentElement < AbstractElement
|
|
4
|
+
attr_reader :template
|
|
5
|
+
|
|
6
|
+
OUTPUT = "@output_buffer.safe_concat(%s);"
|
|
7
|
+
EXPR_STRING = "%s.html_safe"
|
|
8
|
+
|
|
9
|
+
def initialize(*args, template: OUTPUT)
|
|
10
|
+
super(*args)
|
|
11
|
+
@template = template
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def precompile
|
|
15
|
+
[ComponentElement.new(name, precompile_members, precompile_children)]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def compile
|
|
19
|
+
templates = Grsx.configuration.component_rendering_templates
|
|
20
|
+
|
|
21
|
+
tag = templates[:component] % {
|
|
22
|
+
component_class: name,
|
|
23
|
+
view_context: "self",
|
|
24
|
+
kwargs: compile_members,
|
|
25
|
+
children_block: children.any? ? templates[:children] % { children: children.map(&:compile).join } : ""
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if Grsx.configuration.enable_context
|
|
29
|
+
tag = "(grsx_context.push({});#{tag}.tap{grsx_context.pop})"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
template % tag
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def compile_members
|
|
36
|
+
members.each_with_object("") do |member, result|
|
|
37
|
+
case member
|
|
38
|
+
when ExpressionGroup
|
|
39
|
+
result << "**#{member.compile}.transform_keys { |k| ActiveSupport::Inflector.underscore(k).to_sym },"
|
|
40
|
+
when Newline
|
|
41
|
+
result << member.compile
|
|
42
|
+
else
|
|
43
|
+
result << "#{member.compile},"
|
|
44
|
+
end
|
|
45
|
+
end.gsub(/,\z/, "")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def precompile_members
|
|
51
|
+
members.map do |node|
|
|
52
|
+
if node.is_a? ExpressionGroup
|
|
53
|
+
ExpressionGroup.new(
|
|
54
|
+
node.members,
|
|
55
|
+
inner_template: ExpressionGroup::SUB_EXPR,
|
|
56
|
+
outer_template: ExpressionGroup::SUB_EXPR
|
|
57
|
+
)
|
|
58
|
+
else
|
|
59
|
+
node
|
|
60
|
+
end
|
|
61
|
+
end.map(&:precompile).flatten
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def precompile_children
|
|
65
|
+
compact(children.map(&:precompile).flatten)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
module Nodes
|
|
3
|
+
class ComponentProp < AbstractAttr
|
|
4
|
+
def precompile
|
|
5
|
+
[ComponentProp.new(name, precompile_value)]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def compile
|
|
9
|
+
key = ActiveSupport::Inflector.underscore(name)
|
|
10
|
+
"#{key}: #{value.compile}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def precompile_value
|
|
16
|
+
node = value.precompile.first
|
|
17
|
+
|
|
18
|
+
case node
|
|
19
|
+
when Raw
|
|
20
|
+
Raw.new(node.content, template: Raw::EXPR_STRING)
|
|
21
|
+
when ExpressionGroup
|
|
22
|
+
ExpressionGroup.new(node.members, outer_template: ExpressionGroup::SUB_EXPR, inner_template: node.inner_template)
|
|
23
|
+
else
|
|
24
|
+
node
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
module Nodes
|
|
3
|
+
class ExpressionGroup < AbstractNode
|
|
4
|
+
attr_accessor :members
|
|
5
|
+
attr_reader :outer_template, :inner_template
|
|
6
|
+
|
|
7
|
+
def initialize(members, outer_template: nil, inner_template: nil)
|
|
8
|
+
@members = members
|
|
9
|
+
@outer_template = outer_template
|
|
10
|
+
@inner_template = inner_template
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
module Nodes
|
|
3
|
+
# A Fragment node renders its children without a wrapping element.
|
|
4
|
+
# It corresponds to the JSX <></> syntax.
|
|
5
|
+
#
|
|
6
|
+
# In JSX:
|
|
7
|
+
# <>
|
|
8
|
+
# <h1>Title</h1>
|
|
9
|
+
# <p>Body</p>
|
|
10
|
+
# </>
|
|
11
|
+
#
|
|
12
|
+
# Compiled Phlex DSL: just the children inline, no wrapper.
|
|
13
|
+
class Fragment < AbstractNode
|
|
14
|
+
attr_accessor :children
|
|
15
|
+
|
|
16
|
+
def initialize(children)
|
|
17
|
+
@children = children
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# ActionView codegen: compile children directly, no wrapper tag
|
|
21
|
+
def precompile
|
|
22
|
+
children.map(&:precompile).flatten
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def compile
|
|
26
|
+
children.map(&:compile).join
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
module Nodes
|
|
3
|
+
class HTMLElement < AbstractElement
|
|
4
|
+
# Referenced from https://html.spec.whatwg.org/#void-elements
|
|
5
|
+
HTML_VOID_ELEMENTS = %w(area base br col embed hr img input link meta source track wbr)
|
|
6
|
+
|
|
7
|
+
def precompile
|
|
8
|
+
nodes = []
|
|
9
|
+
|
|
10
|
+
if void? && children.length == 0
|
|
11
|
+
nodes.concat(precompile_open_tag)
|
|
12
|
+
else
|
|
13
|
+
nodes.concat(precompile_open_tag)
|
|
14
|
+
nodes.concat(children.map(&:precompile).flatten)
|
|
15
|
+
nodes << Raw.new("</#{name}>")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
nodes
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def void?
|
|
24
|
+
HTML_VOID_ELEMENTS.include?(name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def precompile_open_tag
|
|
28
|
+
nodes = [Raw.new("<#{name}")]
|
|
29
|
+
nodes.concat(precompile_members)
|
|
30
|
+
nodes << Raw.new(">")
|
|
31
|
+
nodes
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def precompile_members
|
|
35
|
+
members.map do |node|
|
|
36
|
+
if node.is_a? ExpressionGroup
|
|
37
|
+
ExpressionGroup.new(
|
|
38
|
+
node.members,
|
|
39
|
+
inner_template: "Grsx::Runtime.splat_attrs(%s)",
|
|
40
|
+
outer_template: ExpressionGroup::OUTPUT_SAFE
|
|
41
|
+
)
|
|
42
|
+
else
|
|
43
|
+
node
|
|
44
|
+
end
|
|
45
|
+
end.map(&:precompile).flatten
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
module Nodes
|
|
3
|
+
class Raw < AbstractNode
|
|
4
|
+
attr_reader :content, :template
|
|
5
|
+
|
|
6
|
+
OUTPUT = "@output_buffer.safe_concat('%s'.freeze);"
|
|
7
|
+
EXPR_STRING = "'%s'.html_safe.freeze"
|
|
8
|
+
|
|
9
|
+
def initialize(content, template: OUTPUT)
|
|
10
|
+
@content = content
|
|
11
|
+
@template = template
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compile
|
|
15
|
+
template % content
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def merge(other_raw)
|
|
19
|
+
content << other_raw.content
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Grsx
|
|
2
|
+
module Nodes
|
|
3
|
+
class Root < AbstractNode
|
|
4
|
+
attr_accessor :children
|
|
5
|
+
|
|
6
|
+
def initialize(children)
|
|
7
|
+
@children = children
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def precompile
|
|
11
|
+
Root.new(compact(children.map(&:precompile).flatten))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compile
|
|
15
|
+
"#{children.map(&:compile).join}@output_buffer.to_s"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|