rbexy 1.1.0 → 2.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +3 -1
  3. data/README.md +19 -1
  4. data/lib/rbexy.rb +26 -9
  5. data/lib/rbexy/ast_transformer.rb +21 -0
  6. data/lib/rbexy/cache_component.rb +17 -0
  7. data/lib/rbexy/component.rb +35 -21
  8. data/lib/rbexy/component_resolver.rb +60 -0
  9. data/lib/rbexy/configuration.rb +18 -1
  10. data/lib/rbexy/lexer.rb +21 -14
  11. data/lib/rbexy/nodes.rb +15 -135
  12. data/lib/rbexy/nodes/abstract_attr.rb +12 -0
  13. data/lib/rbexy/nodes/abstract_element.rb +13 -0
  14. data/lib/rbexy/nodes/abstract_node.rb +58 -0
  15. data/lib/rbexy/nodes/component_element.rb +69 -0
  16. data/lib/rbexy/nodes/component_prop.rb +29 -0
  17. data/lib/rbexy/nodes/declaration.rb +15 -0
  18. data/lib/rbexy/nodes/expression.rb +15 -0
  19. data/lib/rbexy/nodes/expression_group.rb +55 -0
  20. data/lib/rbexy/nodes/html_attr.rb +13 -0
  21. data/lib/rbexy/nodes/html_element.rb +48 -0
  22. data/lib/rbexy/nodes/newline.rb +9 -0
  23. data/lib/rbexy/nodes/raw.rb +23 -0
  24. data/lib/rbexy/nodes/root.rb +19 -0
  25. data/lib/rbexy/nodes/text.rb +15 -0
  26. data/lib/rbexy/nodes/util.rb +9 -0
  27. data/lib/rbexy/parser.rb +22 -16
  28. data/lib/rbexy/rails.rb +2 -0
  29. data/lib/rbexy/rails/component_template_resolver.rb +59 -4
  30. data/lib/rbexy/rails/controller_helper.rb +0 -6
  31. data/lib/rbexy/rails/engine.rb +3 -7
  32. data/lib/rbexy/rails/rbx_dependency_tracker.rb +37 -0
  33. data/lib/rbexy/refinements.rb +5 -0
  34. data/lib/rbexy/refinements/array.rb +9 -0
  35. data/lib/rbexy/refinements/array/find_map.rb +13 -0
  36. data/lib/rbexy/refinements/array/insert_between_types.rb +26 -0
  37. data/lib/rbexy/refinements/array/map_type_when_neighboring_type.rb +26 -0
  38. data/lib/rbexy/runtime.rb +15 -23
  39. data/lib/rbexy/template.rb +12 -0
  40. data/lib/rbexy/version.rb +1 -1
  41. data/rbexy.gemspec +1 -0
  42. metadata +43 -11
  43. data/example.rb +0 -113
  44. data/lib/rbexy/component_providers/namespaced_rbexy_provider.rb +0 -20
  45. data/lib/rbexy/component_providers/rbexy_provider.rb +0 -21
  46. data/lib/rbexy/component_providers/view_component_provider.rb +0 -21
  47. data/lib/rbexy/component_tag_builder.rb +0 -19
  48. data/lib/rbexy/hash_mash.rb +0 -15
  49. data/lib/rbexy/view_context_helper.rb +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6728982815c307bb37a495c3d11450e9582bf1cd17eef5306c9e940c0c36eea
4
- data.tar.gz: 5fdfa1cc3c5fc70b573aa730f827b2f5d38b8031f1adc6344b31926ce520b4af
3
+ metadata.gz: 5d36622639519d2abe9b9306612de65de836294ad529109d0ed5c1f8f11c0750
4
+ data.tar.gz: bae1042c6e80c5ec1a4f609162eee4d1efc66f340a6e742bc22d315bf486f597
5
5
  SHA512:
6
- metadata.gz: 46385e65a3cd976497efd8d6db789eee4a2c73668e085dd6772d8c6a9a835783f6bb80ef923463af97cfec5d5625fffd641229a5a97a1c24e024b75d996969af
7
- data.tar.gz: 025e25a41d9cc5e848342c668721cdef046ab13e5cff9737acc69a66eaca6cdd3ac06493bfcbb42b735fcbd6e7ae243e8a65588d648d3442924306944addb714
6
+ metadata.gz: 749d66a40e869ee18c086d1d1c299936fb8c65ab742492a575a6d1afbc400a8c553df5cb337edf57b43aa200e856ec38b4c2bf6acee3d458a2dcfb86831f4cf7
7
+ data.tar.gz: 88a13df06a488a65ebf7e6aad95051ae23fad394f628e66e667c2ed1584a4e6c33909209e11ccdb210fbc6f41c089f1ce8cef3b80283e099998290c7608e924e
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rbexy (1.1.0)
4
+ rbexy (2.0.0.beta5)
5
5
  actionview (>= 6.0, < 7.0)
6
6
  activesupport (>= 6.0, < 7.0)
7
7
 
@@ -101,6 +101,7 @@ GEM
101
101
  mini_mime (>= 0.1.1)
102
102
  marcel (0.3.3)
103
103
  mimemagic (~> 0.3.2)
104
+ memory_profiler (0.9.14)
104
105
  method_source (1.0.0)
105
106
  mimemagic (0.3.5)
106
107
  mini_mime (1.0.2)
@@ -199,6 +200,7 @@ PLATFORMS
199
200
 
200
201
  DEPENDENCIES
201
202
  guard-rspec (~> 4.7, >= 4.7.3)
203
+ memory_profiler (~> 0.9.14)
202
204
  pry-byebug
203
205
  rails (>= 6.0, < 7.0)
204
206
  rake
data/README.md CHANGED
@@ -173,6 +173,14 @@ Loops:
173
173
  </ul>
174
174
  ```
175
175
 
176
+ Blocks:
177
+
178
+ ```jsx
179
+ {link_to "/" do
180
+ <span>Click me</span>
181
+ end}
182
+ ```
183
+
176
184
  As an attribute:
177
185
 
178
186
  ```jsx
@@ -189,6 +197,16 @@ Pass a lambda to a prop, that when called returns a tag:
189
197
  </Hero>
190
198
  ```
191
199
 
200
+ _Note that when using tags inside blocks, the block must evaluate to a single root element. Rbexy behaves similar to JSX in this way. E.g.:_
201
+
202
+ ```
203
+ # Do
204
+ -> { <span><i>Hello</i> World</span> }
205
+
206
+ # Don't
207
+ -> { <i>Hello</i> World }
208
+ ```
209
+
192
210
  ### Tags
193
211
 
194
212
  You can put standard HTML tags anywhere.
@@ -283,7 +301,7 @@ By default, we'll look for a template file in the same directory as the class an
283
301
  <h1>{@title}</h1>
284
302
  ```
285
303
 
286
- You can call this component from another `.rbx` template file (`<PageHeader title="Hello" />`)—either one rendered by another component class or a Rails view file like `app/views/products/index.rbx`. Or you can call it from ERB (or any other template language) like `PageHeaderComponent.new(self, title: "Hello").render`.
304
+ You can call this component from another `.rbx` template file (`<PageHeader title="Hello" />`)—either one rendered by another component class or a Rails view file like `app/views/products/index.rbx`. Or you can call it from ERB (or any other template language) like `PageHeaderComponent.new(self, title: "Hello").render_in`.
287
305
 
288
306
  Your components and their templates run in the same context as traditional Rails views, so you have access to all of the view helpers you're used to as well as any custom helpers you've defined in `app/helpers/`.
289
307
 
@@ -1,15 +1,22 @@
1
1
  require "rbexy/version"
2
+ require "active_support/inflector"
3
+ require "active_support/concern"
4
+ require "action_view/helpers/output_safety_helper"
5
+ require "action_view/helpers/capture_helper"
6
+ require "action_view/helpers/tag_helper"
7
+ require "action_view/context"
2
8
 
3
9
  module Rbexy
4
10
  autoload :Lexer, "rbexy/lexer"
5
11
  autoload :Parser, "rbexy/parser"
6
12
  autoload :Nodes, "rbexy/nodes"
7
13
  autoload :Runtime, "rbexy/runtime"
8
- autoload :HashMash, "rbexy/hash_mash"
9
- autoload :ComponentTagBuilder, "rbexy/component_tag_builder"
10
- autoload :ViewContextHelper, "rbexy/view_context_helper"
11
14
  autoload :ComponentContext, "rbexy/component_context"
12
15
  autoload :Configuration, "rbexy/configuration"
16
+ autoload :ComponentResolver, "rbexy/component_resolver"
17
+ autoload :Template, "rbexy/template"
18
+ autoload :Refinements, "rbexy/refinements"
19
+ autoload :ASTTransformer, "rbexy/ast_transformer"
13
20
 
14
21
  ContextNotFound = Class.new(StandardError)
15
22
 
@@ -22,14 +29,24 @@ module Rbexy
22
29
  @configuration ||= Configuration.new
23
30
  end
24
31
 
25
- def compile(template_string)
26
- tokens = Rbexy::Lexer.new(template_string).tokenize
27
- template = Rbexy::Parser.new(tokens).parse
28
- template.compile
32
+ def compile(template, context = build_default_compile_context(template))
33
+ tokens = Lexer.new(template, context.element_resolver).tokenize
34
+ root = Parser.new(tokens).parse
35
+ root.inject_compile_context(context)
36
+ root.transform!
37
+ root.precompile.compile
29
38
  end
30
39
 
31
- def evaluate(template_string, runtime)
32
- runtime.evaluate compile(template_string)
40
+ def evaluate(template_string, runtime = Rbexy::Runtime.new)
41
+ runtime.evaluate compile(Template.new(template_string))
42
+ end
43
+
44
+ def build_default_compile_context(template)
45
+ OpenStruct.new(
46
+ template: template,
47
+ element_resolver: configuration.element_resolver,
48
+ ast_transformer: configuration.transforms
49
+ )
33
50
  end
34
51
  end
35
52
  end
@@ -0,0 +1,21 @@
1
+ module Rbexy
2
+ class ASTTransformer
3
+ attr_reader :registry
4
+
5
+ def initialize
6
+ clear!
7
+ end
8
+
9
+ def register(*node_classes, &block)
10
+ node_classes.each { |k| (registry[k] ||= []) << block }
11
+ end
12
+
13
+ def transform(node, context)
14
+ registry[node.class]&.each { |t| t.call(node, context) }
15
+ end
16
+
17
+ def clear!
18
+ @registry = {}
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module Rbexy
2
+ class CacheComponent < Rbexy::Component
3
+ def setup(key:)
4
+ @key = key
5
+ end
6
+
7
+ def call
8
+ @current_template = view_context.instance_variable_get(:@current_template)
9
+
10
+ capture do
11
+ cache @key, virtual_path: @current_template.virtual_path do
12
+ @output_buffer << content
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -11,12 +11,22 @@ module Rbexy
11
11
  end
12
12
  end
13
13
 
14
- def self.component_name
15
- name.underscore
14
+ class_attribute :component_file_location
15
+
16
+ def self.inherited(klass)
17
+ klass.component_file_location = caller_locations(1, 10).reject { |l| l.label == "inherited" }[0].absolute_path
18
+ end
19
+
20
+ def self.template_path
21
+ TemplatePath.new(component_name)
16
22
  end
17
23
 
18
- def component_name
19
- self.class.component_name
24
+ def self.call_component?
25
+ method_defined?(:call)
26
+ end
27
+
28
+ def self.component_name
29
+ name.underscore
20
30
  end
21
31
 
22
32
  def initialize(view_context, **props)
@@ -28,7 +38,7 @@ module Rbexy
28
38
 
29
39
  @view_context = view_context
30
40
 
31
- setup(**props)
41
+ after_initialize(**props)
32
42
  end
33
43
 
34
44
  # Override in your subclass to handle props, setup your component, etc.
@@ -36,18 +46,9 @@ module Rbexy
36
46
  # call super(view_context).
37
47
  def setup(**props); end
38
48
 
39
- def render(&block)
49
+ def render_in(_context = view_context, &block)
40
50
  @content_block = block_given? ? block : nil
41
- call
42
- end
43
-
44
- def call
45
- path = TemplatePath.new(component_name)
46
- template = view_context.lookup_context.find(path)
47
- template.render(self, {})
48
- rescue ActionView::Template::Error => error
49
- error.set_backtrace clean_template_backtrace(error.backtrace)
50
- raise error
51
+ self.class.call_component? ? call : _render
51
52
  end
52
53
 
53
54
  def content
@@ -62,17 +63,30 @@ module Rbexy
62
63
 
63
64
  attr_reader :view_context, :content_block
64
65
 
66
+ def _render
67
+ path = self.class.template_path
68
+ template = view_context.lookup_context.find(path)
69
+ template.render(self, {})
70
+ rescue ActionView::Template::Error => error
71
+ error.set_backtrace clean_template_backtrace(error.backtrace)
72
+ raise error
73
+ end
74
+
65
75
  def method_missing(meth, *args, &block)
66
- if view_context.respond_to?(meth)
67
- view_context.send(meth, *args, &block)
68
- else
69
- super
70
- end
76
+ view_context.send(meth, *args, &block)
77
+ end
78
+
79
+ def respond_to_missing?(method_name, include_all)
80
+ view_context.respond_to?(method_name, include_all)
71
81
  end
72
82
 
73
83
  def clean_template_backtrace(backtrace)
74
84
  return backtrace if Rbexy.configuration.debug
75
85
  BacktraceCleaner.new(backtrace).call
76
86
  end
87
+
88
+ def after_initialize(**props)
89
+ setup(**props)
90
+ end
77
91
  end
78
92
  end
@@ -0,0 +1,60 @@
1
+ module Rbexy
2
+ class ComponentResolver
3
+ using Rbexy::Refinements::Array::FindMap
4
+
5
+ KNOWN_HTML_ELEMENTS = %w(
6
+ a abbr acronym address animate animateMotion animateTransform applet area article aside audio b base basefont
7
+ bdi bdo bgsound big blink blockquote body br button canvas caption center circle cite clipPath code col colgroup
8
+ color-profile command content data datalist dd defs del desc details dfn dialog dir discard div dl dt element
9
+ ellipse em embed feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting
10
+ feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage
11
+ feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence
12
+ fieldset figcaption figure filter font footer foreignObject form frame frameset g h1 h2 h3 h4 h5 h6 hatch
13
+ hatchpath head header hgroup hr html i iframe image img input ins isindex kbd keygen label legend li line
14
+ linearGradient link listing main map mark marker marquee mask menu menuitem mesh meshgradient meshpatch meshrow
15
+ meta metadata meter mpath multicol nav nextid nobr noembed noframes noscript object ol optgroup option output p
16
+ param path pattern picture plaintext polygon polyline pre progress q radialGradient rb rect rp rt rtc ruby s
17
+ samp script section select set shadow slot small solidcolor source spacer span stop strike strong style sub
18
+ summary sup svg switch symbol table tbody td template text textarea textPath tfoot th thead time title tr track
19
+ tspan tt u ul unknown use var video view wbr xmp
20
+ ).to_set
21
+
22
+ def self.try_constantize
23
+ yield
24
+ rescue NameError => e
25
+ raise e unless e.message =~ /wrong constant name/ || e.message =~ /uninitialized constant/
26
+ nil
27
+ end
28
+
29
+ attr_reader :component_namespaces
30
+
31
+ def initialize
32
+ self.component_namespaces = {}
33
+ end
34
+
35
+ def component_namespaces=(hash)
36
+ @component_namespaces = hash.transform_keys(&:to_s)
37
+ end
38
+
39
+ def component?(name, template)
40
+ return false if KNOWN_HTML_ELEMENTS.include?(name)
41
+ return true if component_class(name, template)
42
+ false
43
+ end
44
+
45
+ def component_class(name, template)
46
+ possible_names = matching_namespaces(template).map { |ns| "#{ns}.#{name}" } << name
47
+ possible_names.find_map(&method(:find))
48
+ end
49
+
50
+ private
51
+
52
+ def find(name)
53
+ self.class.try_constantize { ActiveSupport::Inflector.constantize("#{name.gsub(".", "::")}Component") }
54
+ end
55
+
56
+ def matching_namespaces(template)
57
+ component_namespaces.select { |path, ns| template.identifier.start_with?(path) }.values.flatten.uniq
58
+ end
59
+ end
60
+ end
@@ -1,12 +1,29 @@
1
1
  module Rbexy
2
2
  class Configuration
3
- attr_accessor :component_provider
3
+ attr_accessor :element_resolver
4
4
  attr_accessor :template_paths
5
5
  attr_accessor :enable_context
6
6
  attr_accessor :debug
7
+ attr_accessor :component_rendering_templates
8
+ attr_accessor :transforms
7
9
 
8
10
  def template_paths
9
11
  @template_paths ||= []
10
12
  end
13
+
14
+ def element_resolver
15
+ @element_resolver ||= ComponentResolver.new
16
+ end
17
+
18
+ def transforms
19
+ @transforms ||= ASTTransformer.new
20
+ end
21
+
22
+ def component_rendering_templates
23
+ @component_rendering_templates ||= {
24
+ children: "{capture{%{children}}}",
25
+ component: "::%{component_class}.new(%{view_context},%{kwargs}).render_in%{children_block}"
26
+ }
27
+ end
11
28
  end
12
29
  end
@@ -10,7 +10,7 @@ module Rbexy
10
10
  end
11
11
  end
12
12
 
13
- Patterns = HashMash.new(
13
+ Patterns = OpenStruct.new(
14
14
  open_expression: /{/,
15
15
  close_expression: /}/,
16
16
  expression_content: /[^}{"'<]+/,
@@ -29,23 +29,23 @@ module Rbexy
29
29
  single_quote: /'/,
30
30
  double_quoted_text_content: /[^"]+/,
31
31
  single_quoted_text_content: /[^']+/,
32
- expression_internal_tag_prefixes: /(\s+(&&|\?|:|do|do\s*\|[^\|]+\||{|{\s*\|[^\|]+\|)\s+\z|\A\s*\z)/,
32
+ expression_internal_tag_prefixes: /(\s+(&&|\|\||\?|:|do|do\s*\|[^\|]+\||{|{\s*\|[^\|]+\|)\s+\z|\A\s*\z)/,
33
33
  declaration: /<![^>]*>/
34
34
  )
35
35
 
36
- attr_reader :stack, :tokens, :scanner, :curr_expr_quote_levels
37
- attr_accessor :curr_expr_bracket_levels, :curr_expr, :curr_default_text,
36
+ attr_reader :stack, :tokens, :scanner, :element_resolver, :template
37
+ attr_accessor :curr_expr, :curr_default_text,
38
38
  :curr_quoted_text
39
39
 
40
- def initialize(code)
40
+ def initialize(template, element_resolver)
41
+ @template = template
42
+ @scanner = StringScanner.new(template.source)
43
+ @element_resolver = element_resolver
41
44
  @stack = [:default]
42
- @curr_expr_bracket_levels = 0
43
- @curr_expr_quote_levels = { single: 0, double: 0 }
44
45
  @curr_expr = ""
45
46
  @curr_default_text = ""
46
47
  @curr_quoted_text = ""
47
48
  @tokens = []
48
- @scanner = StringScanner.new(code)
49
49
  end
50
50
 
51
51
  def tokenize
@@ -59,7 +59,7 @@ module Rbexy
59
59
  elsif scanner.scan(Patterns.open_expression)
60
60
  open_expression
61
61
  elsif scanner.scan(Patterns.comment)
62
- tokens << [:SILENT_NEWLINE]
62
+ tokens << [:NEWLINE]
63
63
  elsif scanner.check(Patterns.text_content)
64
64
  stack.push(:default_text)
65
65
  else
@@ -74,7 +74,7 @@ module Rbexy
74
74
  elsif scanner.scan(Patterns.open_expression)
75
75
  open_expression
76
76
  elsif scanner.scan(Patterns.comment)
77
- tokens << [:SILENT_NEWLINE]
77
+ tokens << [:NEWLINE]
78
78
  elsif scanner.check(Patterns.text_content)
79
79
  stack.push(:default_text)
80
80
  else
@@ -163,9 +163,9 @@ module Rbexy
163
163
  tokens << [:CLOSE_TAG_DEF]
164
164
  stack.pop
165
165
  elsif scanner.scan(Patterns.tag_name)
166
- tokens << [:TAG_NAME, scanner.matched]
166
+ tokens << [:TAG_DETAILS, tag_details(scanner.matched)]
167
167
  elsif scanner.scan(Patterns.whitespace)
168
- scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
168
+ scanner.matched.count("\n").times { tokens << [:NEWLINE] }
169
169
  tokens << [:OPEN_ATTRS]
170
170
  stack.push(:tag_attrs)
171
171
  else
@@ -182,7 +182,7 @@ module Rbexy
182
182
  end
183
183
  when :tag_attrs
184
184
  if scanner.scan(Patterns.whitespace)
185
- scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
185
+ scanner.matched.count("\n").times { tokens << [:NEWLINE] }
186
186
  elsif scanner.check(Patterns.close_tag)
187
187
  tokens << [:CLOSE_ATTRS]
188
188
  stack.pop
@@ -205,7 +205,7 @@ module Rbexy
205
205
  open_expression
206
206
  elsif scanner.scan(Patterns.whitespace) || scanner.check(Patterns.close_tag)
207
207
  tokens << [:CLOSE_ATTR_VALUE]
208
- scanner.matched.count("\n").times { tokens << [:SILENT_NEWLINE] }
208
+ scanner.matched.count("\n").times { tokens << [:NEWLINE] }
209
209
  stack.pop
210
210
  else
211
211
  raise SyntaxError, self
@@ -286,5 +286,12 @@ module Rbexy
286
286
  # etc).
287
287
  scanner.scan(Patterns.expression_content) || scanner.scan(Patterns.open_tag_end)
288
288
  end
289
+
290
+ def tag_details(name)
291
+ type = element_resolver.component?(name, template) ? :component : :html
292
+ details = { name: scanner.matched, type: type }
293
+ details[:component_class] = element_resolver.component_class(name, template) if type == :component
294
+ details
295
+ end
289
296
  end
290
297
  end