rbexy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rbexy"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ version: '3'
2
+
3
+ volumes:
4
+ bundle:
5
+
6
+ services:
7
+ rbexy:
8
+ build: .
9
+ volumes:
10
+ - .:/app
11
+ - bundle:/usr/local/bundle
12
+ - $HOME/.ssh:/root/.ssh:ro
13
+ - $HOME/.gitconfig:/root/.gitconfig:ro
14
+ - $HOME/.gem/credentials:/root/.gem/credentials
15
+ working_dir: /app
@@ -0,0 +1,113 @@
1
+ require "bundler"
2
+ Bundler.require
3
+
4
+ require "active_support/all"
5
+ require "action_view/helpers"
6
+ require "action_view/context"
7
+ require "action_view/buffers"
8
+
9
+ template_string = <<-RBX
10
+ # A comment here
11
+ <div>
12
+ <h1 {**{ class: "myClass" }} {**splat_attrs}>Hello world</h1>
13
+ <div {**{ class: "myClass" }}></div>
14
+ Some words
15
+ # A comment here
16
+ # A comment here
17
+ <p>Lorem ipsum</p>
18
+ <input type="submit" value={@ivar_val} disabled />
19
+ {true && <p>Is true</p>}
20
+ {false && <p>Is false</p>}
21
+ {true ? <p {**{ class: "myClass" }}>Ternary is {'true'.upcase}</p> : <p>Ternary is false</p>}
22
+ <Button prop1="val1" prop2={true && "val2"} multi-word-prop="value">the content</Button>
23
+ <Forms.TextField label={->(n) { <label id={n}>Something</label> }} note={<p>the note</p>} />
24
+ <ul>
25
+ # A comment here
26
+ {["hi", "there", "nick"].map { |val| <li>{val}</li> }}
27
+ </ul>
28
+ <p
29
+ class="something">Text</p>
30
+ <input
31
+ class="foobar"
32
+ />
33
+ <div
34
+ with="lots"
35
+ of="attributes"
36
+ >
37
+ Content
38
+ </div>
39
+ # comment
40
+ </div>
41
+ # comment
42
+ RBX
43
+
44
+ module Components
45
+ class ButtonComponent
46
+ def initialize(prop1:, prop2:, multi_word_prop:)
47
+ @prop1 = prop1
48
+ @prop2 = prop2
49
+ @multi_word_prop = multi_word_prop
50
+ end
51
+
52
+ def render
53
+ # Render it yourself, call one of Rails view helpers (link_to,
54
+ # content_tag, etc), or use a template file. Be sure to render
55
+ # children by yielding to the given block.
56
+ "<button class=\"#{[@prop1, @prop2, @multi_word_prop].join("-")}\">#{yield}</button>"
57
+ end
58
+ end
59
+
60
+ module Forms
61
+ class TextFieldComponent
62
+ def initialize(label:, note:, **attrs)
63
+ @label = label
64
+ @note = note
65
+ end
66
+
67
+ def render
68
+ "#{@label.call(2)} <input type=\"text\" />#{@note}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ class ComponentProvider
75
+ def match?(name)
76
+ find(name) != nil
77
+ end
78
+
79
+ def render(context, name, **attrs, &block)
80
+ props = attrs.transform_keys { |k| ActiveSupport::Inflector.underscore(k.to_s).to_sym }
81
+ find(name).new(**props).render(&block)
82
+ end
83
+
84
+ private
85
+
86
+ def find(name)
87
+ ActiveSupport::Inflector.constantize("Components::#{name}Component")
88
+ rescue NameError => e
89
+ raise e unless e.message =~ /constant/
90
+ nil
91
+ end
92
+ end
93
+
94
+ class MyRuntime < Rbexy::Runtime
95
+ def initialize(component_provider)
96
+ super(component_provider)
97
+ @ivar_val = "ivar value"
98
+ end
99
+
100
+ def splat_attrs
101
+ {
102
+ key1: "val1",
103
+ key2: "val2"
104
+ }
105
+ end
106
+ end
107
+
108
+ puts "=============== Compiled ruby code ==============="
109
+ code = Rbexy.compile(template_string)
110
+ puts code
111
+
112
+ puts "=============== Result of eval ==============="
113
+ puts MyRuntime.new(ComponentProvider.new).evaluate(code)
@@ -0,0 +1,35 @@
1
+ require "rbexy/version"
2
+
3
+ module Rbexy
4
+ autoload :Lexer, "rbexy/lexer"
5
+ autoload :Parser, "rbexy/parser"
6
+ autoload :Nodes, "rbexy/nodes"
7
+ autoload :Runtime, "rbexy/runtime"
8
+ autoload :HashMash, "rbexy/hash_mash"
9
+ autoload :OutputBuffer, "rbexy/output_buffer"
10
+ autoload :ComponentTagBuilder, "rbexy/component_tag_builder"
11
+ autoload :ViewContextHelper, "rbexy/view_context_helper"
12
+ autoload :Configuration, "rbexy/configuration"
13
+
14
+ ContextNotFound = Class.new(StandardError)
15
+
16
+ class << self
17
+ def configure
18
+ yield(configuration)
19
+ end
20
+
21
+ def configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+
25
+ def compile(template_string)
26
+ tokens = Rbexy::Lexer.new(template_string).tokenize
27
+ template = Rbexy::Parser.new(tokens).parse
28
+ template.compile
29
+ end
30
+
31
+ def evaluate(template_string, runtime)
32
+ runtime.evaluate compile(template_string)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,100 @@
1
+ require "action_view"
2
+
3
+ module Rbexy
4
+ class Component < ActionView::Base
5
+ class LookupContext < ActionView::LookupContext
6
+ def self.details_hash(context)
7
+ context.registered_details.each_with_object({}) do |key, details_hash|
8
+ value = key == :locale ? [context.locale] : context.send(key)
9
+ details_hash[key] = value
10
+ end
11
+ end
12
+
13
+ # We override any calls to args_for_lookup and set partial=false so that
14
+ # the lookup context doesn't automatically add a `_` prefix to the
15
+ # template path, since we're using the Rails partial-rendering
16
+ # functionality but don't want our templates prefixed with a `_`
17
+ def args_for_lookup(name, prefixes, partial, keys, details_options)
18
+ super(name, prefixes, false, keys, details_options)
19
+ end
20
+ end
21
+
22
+ def initialize(view_context, **props)
23
+ super(
24
+ view_context.lookup_context,
25
+ view_context.assigns,
26
+ view_context.controller
27
+ )
28
+
29
+ @view_context = view_context
30
+
31
+ setup(**props)
32
+ end
33
+
34
+ # Override in your subclass to handle props, setup your component, etc.
35
+ # You can also implement `initialize` but you just need to remember to
36
+ # call super(view_context).
37
+ def setup(**props); end
38
+
39
+ def render(&block)
40
+ @content = nil
41
+ @content_block = block_given? ? block : nil
42
+ call
43
+ end
44
+
45
+ def call
46
+ old_lookup_context = view_renderer.lookup_context
47
+ view_renderer.lookup_context = build_lookup_context(old_lookup_context)
48
+ view_renderer.render(self, partial: component_name, &nil)
49
+ ensure
50
+ view_renderer.lookup_context = old_lookup_context
51
+ end
52
+
53
+ def content
54
+ @content ||= content_block ? view_context.capture(self, &content_block) : ""
55
+ end
56
+
57
+ def create_context(name, value)
58
+ rbexy_context.last[name] = value
59
+ end
60
+
61
+ def use_context(name)
62
+ index = rbexy_context.rindex { |c| c.has_key?(name) }
63
+ index ?
64
+ rbexy_context[index][name] :
65
+ raise(ContextNotFound, "no parent context `#{name}`")
66
+ end
67
+
68
+ private
69
+
70
+ attr_reader :view_context, :content_block
71
+
72
+ def build_lookup_context(existing_context)
73
+ paths = existing_context.view_paths.dup.unshift(
74
+ *Rbexy.configuration.template_paths.map { |p| ActionView::OptimizedFileSystemResolver.new(p) }
75
+ )
76
+
77
+ LookupContext.new(
78
+ paths,
79
+ LookupContext.details_hash(existing_context),
80
+ Rbexy.configuration.template_prefixes
81
+ )
82
+ end
83
+
84
+ def view_renderer
85
+ view_context.view_renderer
86
+ end
87
+
88
+ def component_name
89
+ self.class.name.underscore
90
+ end
91
+
92
+ def method_missing(meth, *args, &block)
93
+ if view_context.respond_to?(meth)
94
+ view_context.send(meth, *args, &block)
95
+ else
96
+ super
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,23 @@
1
+ module Rbexy
2
+ module ComponentProviders
3
+ class RbexyProvider
4
+ def match?(name)
5
+ name =~ /^[A-Z]/ && find(name) != nil
6
+ end
7
+
8
+ def render(context, name, **attrs, &block)
9
+ props = attrs.transform_keys { |k| ActiveSupport::Inflector.underscore(k.to_s).to_sym }
10
+ find(name).new(context, **props).render(&block)
11
+ end
12
+
13
+ private
14
+
15
+ def find(name)
16
+ ActiveSupport::Inflector.constantize("#{name}Component")
17
+ rescue NameError => e
18
+ raise e unless e.message =~ /wrong constant name/ || e.message =~ /uninitialized constant/
19
+ nil
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Rbexy
2
+ module ComponentProviders
3
+ class ViewComponentProvider
4
+ def match?(name)
5
+ name =~ /^[A-Z]/ && find(name) != nil
6
+ end
7
+
8
+ def render(context, name, **attrs, &block)
9
+ props = attrs.transform_keys { |k| ActiveSupport::Inflector.underscore(k.to_s).to_sym }
10
+ find(name).new(**props).render_in(context, &block)
11
+ end
12
+
13
+ private
14
+
15
+ def find(name)
16
+ ActiveSupport::Inflector.constantize("#{name}Component")
17
+ rescue NameError => e
18
+ raise e unless e.message =~ /wrong constant name/ || e.message =~ /uninitialized constant/
19
+ nil
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Rbexy
2
+ class ComponentTagBuilder < ActionView::Helpers::TagHelper::TagBuilder
3
+ attr_reader :component_provider
4
+
5
+ def initialize(context, component_provider)
6
+ super(context)
7
+ @component_provider = component_provider
8
+ end
9
+
10
+ def method_missing(called, *args, **attrs, &block)
11
+ component_name = called.to_s.gsub("__", "::")
12
+ if component_provider.match?(component_name)
13
+ component_provider.render(@view_context, component_name, **attrs, &block)
14
+ else
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Rbexy
2
+ class Configuration
3
+ attr_accessor :component_provider
4
+ attr_accessor :template_paths
5
+ attr_accessor :template_prefixes
6
+
7
+ def template_paths
8
+ @template_paths ||= []
9
+ end
10
+
11
+ def template_prefixes
12
+ @template_prefixes ||= []
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Rbexy
2
+ class HashMash < Hash
3
+ def initialize(hash)
4
+ replace(hash)
5
+ end
6
+
7
+ def method_missing(meth, *args, &block)
8
+ if has_key?(meth)
9
+ self[meth]
10
+ else
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,279 @@
1
+ module Rbexy
2
+ class Lexer
3
+ class SyntaxError < StandardError
4
+ def initialize(lexer)
5
+ super(
6
+ "Invalid syntax: `#{lexer.scanner.peek(20)}`\n" +
7
+ "Stack: #{lexer.stack}\n" +
8
+ "Tokens: #{lexer.tokens}"
9
+ )
10
+ end
11
+ end
12
+
13
+ Patterns = HashMash.new(
14
+ open_expression: /{/,
15
+ close_expression: /}/,
16
+ expression_content: /[^}{"'<]+/,
17
+ open_tag_def: /<(?!\/)/,
18
+ open_tag_end: /<\//,
19
+ close_tag: /\s*\/?>/,
20
+ close_self_closing_tag: /\s*\/>/,
21
+ tag_name: /\/?[A-Za-z0-9\-_.]+/,
22
+ text_content: /[^<{#]+/,
23
+ comment: /^\p{Blank}*#.*(\n|\z)/,
24
+ whitespace: /\s+/,
25
+ attr: /[A-Za-z0-9\-_\.]+/,
26
+ open_attr_splat: /{\*\*/,
27
+ attr_assignment: /=/,
28
+ double_quote: /"/,
29
+ single_quote: /'/,
30
+ double_quoted_text_content: /[^"]+/,
31
+ single_quoted_text_content: /[^']+/,
32
+ expression_internal_tag_prefixes: /(\s+(&&|\?|:|do|do\s*\|[^\|]+\||{|{\s*\|[^\|]+\|)\s+\z|\A\s*\z)/,
33
+ declaration: /<![^>]*>/
34
+ )
35
+
36
+ attr_reader :stack, :tokens, :scanner, :curr_expr_quote_levels
37
+ attr_accessor :curr_expr_bracket_levels, :curr_expr, :curr_default_text,
38
+ :curr_quoted_text
39
+
40
+ def initialize(code)
41
+ @stack = [:default]
42
+ @curr_expr_bracket_levels = 0
43
+ @curr_expr_quote_levels = { single: 0, double: 0 }
44
+ @curr_expr = ""
45
+ @curr_default_text = ""
46
+ @curr_quoted_text = ""
47
+ @tokens = []
48
+ @scanner = StringScanner.new(code)
49
+ end
50
+
51
+ def tokenize
52
+ until scanner.eos?
53
+ case stack.last
54
+ when :default
55
+ if scanner.scan(Patterns.declaration)
56
+ tokens << [:DECLARATION, scanner.matched]
57
+ elsif scanner.scan(Patterns.open_tag_def)
58
+ open_tag_def
59
+ elsif scanner.scan(Patterns.open_expression)
60
+ open_expression
61
+ elsif scanner.scan(Patterns.comment)
62
+ tokens << [:SILENT_NEWLINE]
63
+ elsif scanner.check(Patterns.text_content)
64
+ stack.push(:default_text)
65
+ else
66
+ raise SyntaxError, self
67
+ end
68
+ when :tag
69
+ if scanner.scan(Patterns.open_tag_def)
70
+ open_tag_def
71
+ elsif scanner.scan(Patterns.open_tag_end)
72
+ tokens << [:OPEN_TAG_END]
73
+ stack.push(:tag_end)
74
+ elsif scanner.scan(Patterns.open_expression)
75
+ open_expression
76
+ elsif scanner.scan(Patterns.comment)
77
+ tokens << [:SILENT_NEWLINE]
78
+ elsif scanner.check(Patterns.text_content)
79
+ stack.push(:default_text)
80
+ else
81
+ raise SyntaxError, self
82
+ end
83
+ when :default_text
84
+ if scanner.scan(Patterns.text_content)
85
+ self.curr_default_text += scanner.matched
86
+ if scanner.matched.end_with?('\\') && scanner.peek(1) == "{"
87
+ self.curr_default_text += scanner.getch
88
+ elsif scanner.matched.end_with?('\\') && scanner.peek(1) == "#"
89
+ self.curr_default_text += scanner.getch
90
+ else
91
+ if scanner.peek(1) == "#"
92
+ # If the next token is a comment, trim trailing whitespace from
93
+ # the text value so we don't add to the indentation of the next
94
+ # value that is output after the comment
95
+ self.curr_default_text = curr_default_text.gsub(/^\p{Blank}*\z/, "")
96
+ end
97
+ tokens << [:TEXT, curr_default_text]
98
+ self.curr_default_text = ""
99
+ stack.pop
100
+ end
101
+ else
102
+ raise SyntaxError, self
103
+ end
104
+ when :expression
105
+ if scanner.scan(Patterns.close_expression)
106
+ tokens << [:EXPRESSION_BODY, curr_expr]
107
+ tokens << [:CLOSE_EXPRESSION]
108
+ self.curr_expr = ""
109
+ stack.pop
110
+ elsif scanner.scan(Patterns.open_expression)
111
+ expression_inner_bracket
112
+ elsif scanner.scan(Patterns.double_quote)
113
+ expression_inner_double_quote
114
+ elsif scanner.scan(Patterns.single_quote)
115
+ expression_inner_single_quote
116
+ elsif scanner.scan(Patterns.open_tag_def)
117
+ potential_expression_inner_tag
118
+ elsif scanner.scan(Patterns.expression_content)
119
+ self.curr_expr += scanner.matched
120
+ else
121
+ raise SyntaxError, self
122
+ end
123
+ when :expression_inner_bracket
124
+ if scanner.scan(Patterns.close_expression)
125
+ self.curr_expr += scanner.matched
126
+ stack.pop
127
+ elsif scanner.scan(Patterns.open_expression)
128
+ expression_inner_bracket
129
+ elsif scanner.scan(Patterns.double_quote)
130
+ expression_inner_double_quote
131
+ elsif scanner.scan(Patterns.single_quote)
132
+ expression_inner_single_quote
133
+ elsif scanner.scan(Patterns.open_tag_def)
134
+ potential_expression_inner_tag
135
+ elsif scanner.scan(Patterns.expression_content)
136
+ self.curr_expr += scanner.matched
137
+ else
138
+ raise SyntaxError, self
139
+ end
140
+ when :expression_inner_double_quote
141
+ if scanner.check(Patterns.double_quote)
142
+ expression_quoted_string_content
143
+ elsif scanner.scan(Patterns.double_quoted_text_content)
144
+ self.curr_expr += scanner.matched
145
+ else
146
+ raise SyntaxError, self
147
+ end
148
+ when :expression_inner_single_quote
149
+ if scanner.check(Patterns.single_quote)
150
+ expression_quoted_string_content
151
+ elsif scanner.scan(Patterns.single_quoted_text_content)
152
+ self.curr_expr += scanner.matched
153
+ else
154
+ raise SyntaxError, self
155
+ end
156
+ when :tag_def
157
+ if scanner.scan(Patterns.close_self_closing_tag)
158
+ tokens << [:CLOSE_TAG_DEF]
159
+ tokens << [:OPEN_TAG_END]
160
+ tokens << [:CLOSE_TAG_END]
161
+ stack.pop(2)
162
+ elsif scanner.scan(Patterns.close_tag)
163
+ tokens << [:CLOSE_TAG_DEF]
164
+ stack.pop
165
+ elsif scanner.scan(Patterns.tag_name)
166
+ tokens << [:TAG_NAME, scanner.matched]
167
+ elsif scanner.scan(Patterns.whitespace)
168
+ scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
169
+ tokens << [:OPEN_ATTRS]
170
+ stack.push(:tag_attrs)
171
+ else
172
+ raise SyntaxError, self
173
+ end
174
+ when :tag_end
175
+ if scanner.scan(Patterns.close_tag)
176
+ tokens << [:CLOSE_TAG_END]
177
+ stack.pop(2)
178
+ elsif scanner.scan(Patterns.tag_name)
179
+ tokens << [:TAG_NAME, scanner.matched]
180
+ else
181
+ raise SyntaxError, self
182
+ end
183
+ when :tag_attrs
184
+ if scanner.scan(Patterns.whitespace)
185
+ scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
186
+ elsif scanner.check(Patterns.close_tag)
187
+ tokens << [:CLOSE_ATTRS]
188
+ stack.pop
189
+ elsif scanner.scan(Patterns.attr_assignment)
190
+ tokens << [:OPEN_ATTR_VALUE]
191
+ stack.push(:tag_attr_value)
192
+ elsif scanner.scan(Patterns.attr)
193
+ tokens << [:ATTR_NAME, scanner.matched.strip]
194
+ elsif scanner.scan(Patterns.open_attr_splat)
195
+ tokens << [:OPEN_ATTR_SPLAT]
196
+ tokens << [:OPEN_EXPRESSION]
197
+ stack.push(:tag_attr_splat, :expression)
198
+ else
199
+ raise SyntaxError, self
200
+ end
201
+ when :tag_attr_value
202
+ if scanner.scan(Patterns.double_quote)
203
+ stack.push(:quoted_text)
204
+ elsif scanner.scan(Patterns.open_expression)
205
+ open_expression
206
+ elsif scanner.scan(Patterns.whitespace) || scanner.check(Patterns.close_tag)
207
+ tokens << [:CLOSE_ATTR_VALUE]
208
+ scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
209
+ stack.pop
210
+ else
211
+ raise SyntaxError, self
212
+ end
213
+ when :tag_attr_splat
214
+ # Splat is consumed by :expression. It pops control back to here once
215
+ # it's done, and we just record the completion and pop back to :tag_attrs
216
+ tokens << [:CLOSE_ATTR_SPLAT]
217
+ stack.pop
218
+ when :quoted_text
219
+ if scanner.scan(Patterns.double_quoted_text_content)
220
+ self.curr_quoted_text += scanner.matched
221
+ if scanner.matched.end_with?('\\') && scanner.peek(1) == "\""
222
+ self.curr_quoted_text += scanner.getch
223
+ end
224
+ elsif scanner.scan(Patterns.double_quote)
225
+ tokens << [:TEXT, curr_quoted_text]
226
+ self.curr_quoted_text = ""
227
+ stack.pop
228
+ else
229
+ raise SyntaxError, self
230
+ end
231
+ else
232
+ raise SyntaxError, self
233
+ end
234
+ end
235
+
236
+ tokens
237
+ end
238
+
239
+ def potential_expression_inner_tag
240
+ if self.curr_expr =~ Patterns.expression_internal_tag_prefixes
241
+ tokens << [:EXPRESSION_BODY, curr_expr]
242
+ self.curr_expr = ""
243
+ open_tag_def
244
+ else
245
+ self.curr_expr += scanner.matched
246
+ end
247
+ end
248
+
249
+ def open_tag_def
250
+ tokens << [:OPEN_TAG_DEF]
251
+ stack.push(:tag, :tag_def)
252
+ end
253
+
254
+ def open_expression
255
+ tokens << [:OPEN_EXPRESSION]
256
+ stack.push(:expression)
257
+ end
258
+
259
+ def expression_inner_bracket
260
+ self.curr_expr += scanner.matched
261
+ stack.push(:expression_inner_bracket)
262
+ end
263
+
264
+ def expression_inner_double_quote
265
+ self.curr_expr += scanner.matched
266
+ stack.push(:expression_inner_double_quote)
267
+ end
268
+
269
+ def expression_inner_single_quote
270
+ self.curr_expr += scanner.matched
271
+ stack.push(:expression_inner_single_quote)
272
+ end
273
+
274
+ def expression_quoted_string_content
275
+ self.curr_expr += scanner.getch
276
+ stack.pop unless curr_expr.end_with?('\\')
277
+ end
278
+ end
279
+ end