rbexy 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,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