antlers 1.2.0 → 1.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ce3c9ccc84415933ff474d8187c420e28618fa7c15c56bffe0060e2364750d2
4
- data.tar.gz: cc69421e1365a4c3f38975aadd7eee7390797ba48c2aff399ad425f2bb05160e
3
+ metadata.gz: f686ef1d7e193fd0f89d1193b2eaca591adeb32b4d3ab4921b8f720b344b4d1c
4
+ data.tar.gz: 39c5d31434df9e67ad4ba47e8f3e3af3598baa61a5c4faffee4645361f3a2d44
5
5
  SHA512:
6
- metadata.gz: da91462599377c23cc6cfc92fe4fa74e917a216a6378f1ef353ac3fcd6c53a947644dfc9cb3c2bb3937adaf33c5644d28a1f9698ea570c31de36479a8964301e
7
- data.tar.gz: 6c46c7a4aa39005d09ba125a7b2f4a1129383ade80691fffbe0757c3a19683144ecbbe12392ab22814b5894c83a3d489701ccdaa1da116e927881816e2cc4ad9
6
+ metadata.gz: f4b39cece90d9435ef99132ea4c0fa1e013876343c50266502e2e4a53dfc7116d8a371f2d6ff69a791e01a622ad85ef3c15ecf62e69541630ddbafc51f8a59f7
7
+ data.tar.gz: 8afff52daeebd8f5546d64b0db202e2b75f452cece2d3395873ddf6fac8b1855ba032b433ff876344538dd9a068bb6aed6ce425eb13a43ae2f7e30fdbdef4104
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antlers
4
+ # Gives a method only the keywords it takes like initialize(limit:) and render(event:, slot:).
5
+ # LowNode uses the Method Factory pattern but the *instance* methods behave as the actualy method definitions.
6
+ module Keywords
7
+ module_function
8
+
9
+ # The keywords the method declares, or all of them when it takes **.
10
+ def accepted(method, keywords)
11
+ parameters = method.parameters
12
+ return keywords if parameters.any? { |type, _| type == :keyrest }
13
+
14
+ keywords.slice(*declared(parameters))
15
+ end
16
+
17
+ # Whether the method takes the keyword, by name or with **.
18
+ def takes?(method, name)
19
+ parameters = method.parameters
20
+ parameters.any? { |type, _| type == :keyrest } || declared(parameters).include?(name)
21
+ end
22
+
23
+ def declared(parameters)
24
+ parameters.filter_map { |type, name| name if %i[key keyreq].include?(type) }
25
+ end
26
+ end
27
+ end
data/lib/antlers/lexer.rb CHANGED
@@ -64,8 +64,9 @@ module Antlers
64
64
  raise LexerError, "Unrecognised syntax: '#{segment}'"
65
65
  end
66
66
 
67
+ # Keywords are whole words, so a name like "Plain::Card" isn't split on the "in:" in it.
67
68
  def parse_antlers_segment(segment:)
68
- name_and_props, *keywords = segment.split(/(#{Regexp.union(@keywords)})/)
69
+ name_and_props, *keywords = segment.split(/(?<!\S)(#{Regexp.union(@keywords)})(?!\S)/)
69
70
  name, *props = name_and_props.split(' ')
70
71
  [name, props, parse_keywords(keywords:)]
71
72
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antlers
4
- VERSION = '1.2.0'
4
+ VERSION = '1.2.1'
5
5
  end
data/lib/antlers.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'antlers/elements'
4
+ require_relative 'antlers/keywords'
4
5
  require_relative 'antlers/lexer'
5
6
  require_relative 'antlers/parser'
6
7
 
@@ -17,4 +18,10 @@ module Antlers
17
18
  def render(ast:, current_binding:, parent_binding: nil, slot_node: nil, metadata: {})
18
19
  ast.render(current_binding:, parent_binding:, slot_node:, metadata:)
19
20
  end
21
+
22
+ # Call once an app has loaded, to find prop nodes built before their class loaded.
23
+ # Returns the prop nodes whose class is still undefined, which look up their class when first rendered.
24
+ def resolve_nodes
25
+ PropNode.resolve
26
+ end
20
27
  end
@@ -21,9 +21,7 @@ module Antlers
21
21
 
22
22
  # Support sending an event to render_template hook when lowevent available. See lownode as an example implementation.
23
23
  def possible_render_event(props:)
24
- Low::Events::RenderEvent.new(action: :render, props:) if defined?(LowEvent)
25
-
26
- nil
24
+ Low::Events::RenderEvent.new(action: :render, props:) if defined?(Low::Events::RenderEvent)
27
25
  end
28
26
 
29
27
  class << self
@@ -8,12 +8,13 @@ module Antlers
8
8
  class_from_namespace(namespace:, name:)
9
9
  end
10
10
 
11
+ # Nil when the class isn't defined in the namespace or any of its parents.
11
12
  def class_from_namespace(namespace:, name:)
12
- namespace_with_name = [namespace, name].join('::')
13
+ namespace_with_name = [*namespace, name].join('::')
13
14
  return Object.const_get(namespace_with_name) if Object.const_defined?(namespace_with_name)
15
+ return nil if namespace.empty?
14
16
 
15
- namespace.pop
16
- class_from_namespace(namespace:, name:)
17
+ class_from_namespace(namespace: namespace[0...-1], name:)
17
18
  end
18
19
  end
19
20
  end
data/lib/modules/props.rb CHANGED
@@ -22,7 +22,7 @@ module Antlers
22
22
  evaluated_props = {}
23
23
 
24
24
  props.each do |name, value|
25
- evaluated_props[name] = evaluate(value:, current_binding:)
25
+ evaluated_props[name] = evaluate(value:, current_binding:, escape: false)
26
26
  end
27
27
 
28
28
  evaluated_props
@@ -9,9 +9,10 @@ module Antlers
9
9
 
10
10
  # Evaluation limited to the following for security and to stop a templating language becoming a programming language.
11
11
  # 1. A static string
12
- # 2. An instance variable
13
- # 3. A method call/local variable
14
- # 4. A method chain
12
+ # 2. A number, like 3 or 1.5
13
+ # 3. An instance variable
14
+ # 4. A method call/local variable
15
+ # 5. A method chain
15
16
  def evaluate(value:, current_binding:, escape: true)
16
17
  result = result(value:, current_binding:)
17
18
 
@@ -22,6 +23,7 @@ module Antlers
22
23
 
23
24
  def result(value:, current_binding:)
24
25
  return value[1..-2] if Queries.user_defined_string?(value)
26
+ return Queries.number(value) if Queries.number?(value)
25
27
  return value unless current_binding
26
28
 
27
29
  name, *chain = value.split('.')
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../antlers/keywords'
3
4
  require_relative '../interfaces/leaf_node'
4
5
  require_relative '../modules/namespace'
5
6
  require_relative '../modules/props'
@@ -13,6 +14,14 @@ module Antlers
13
14
  super(name:, props:)
14
15
 
15
16
  @namespace = namespace
17
+
18
+ # A class defined in a file that hasn't loaded yet is pending until "Antlers.resolve_nodes" or its first render.
19
+ PropNode.pending << self unless resolve
20
+ end
21
+
22
+ # Whether the prop's class is defined.
23
+ def resolve
24
+ !node_class.nil?
16
25
  end
17
26
 
18
27
  # Classes referenced via "<{ MyNode }>" must implement class/instance and render/render_template methods (See lownode).
@@ -20,17 +29,28 @@ module Antlers
20
29
  props = evaluate_props(props: @props, current_binding:)
21
30
  event = possible_render_event(props:)
22
31
 
23
- # TODO: Get dirload to load constants defined in "<{ MyNode }>" syntax so that we can resolve namespace/params on class load.
24
- klass = class_constant(namespace: @namespace&.split('::') || [], name: @name)
25
- class_proxy = Lowkey[klass.to_s].first[klass.to_s]
32
+ klass = node_class || raise(NameError, "uninitialized constant #{@name}")
26
33
 
27
- instance = create_instance(class_proxy:, klass:, event:, props:)
28
- return instance.render_template(event:, parent_binding:, props:) if klass.template
34
+ # Initialize and render each get the props they take, and the event if they take it.
35
+ keywords = props.merge(event:)
36
+ instance = klass.new(**Keywords.accepted(klass.instance_method(:initialize), keywords))
37
+ return instance.render_template(event:, parent_binding:, props:) if klass.respond_to?(:template) && klass.template
29
38
 
30
- render_args(class_proxy:, instance:, event:, props:)
39
+ instance.render(**Keywords.accepted(instance.method(:render), keywords))
31
40
  end
32
41
 
33
42
  class << self
43
+ # Prop nodes whose class wasn't defined when their template was built.
44
+ def pending
45
+ @pending ||= []
46
+ end
47
+
48
+ # Resolves pending prop nodes once every class has loaded, returning those whose class is still undefined.
49
+ def resolve
50
+ pending.reject!(&:resolve)
51
+ pending
52
+ end
53
+
34
54
  def match?(segment:)
35
55
  segment[:prop]
36
56
  end
@@ -42,22 +62,8 @@ module Antlers
42
62
 
43
63
  private
44
64
 
45
- def create_instance(class_proxy:, klass:, event:, props:)
46
- initialize_params = class_proxy.instance_methods[:initialize]&.tagged_params(:keyword)&.map(&:name) || []
47
-
48
- return klass.new(event:, **props) if initialize_params.include?(:event) && initialize_params.count > 1
49
- return klass.new(**props) if initialize_params.count > 1
50
- return klass.new(event:) if initialize_params.include?(:event)
51
-
52
- klass.new
53
- end
54
-
55
- def render_args(class_proxy:, instance:, event:, props:)
56
- render_params = class_proxy.instance_methods[:render]&.tagged_params(:keyword)&.map(&:name) || []
57
- return instance.render(event:, **props) if render_params.include?(:event) && render_params.count > 1
58
- return instance.render(event:) if render_params.include?(:event)
59
-
60
- instance.render
65
+ def node_class
66
+ class_constant(namespace: @namespace&.split('::') || [], name: @name)
61
67
  end
62
68
  end
63
69
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../antlers/keywords'
3
4
  require_relative '../interfaces/branch_node'
4
5
  require_relative '../modules/namespace'
5
6
  require_relative '../modules/props'
@@ -24,14 +25,24 @@ module Antlers
24
25
  props = evaluate_props(props: @props, current_binding:)
25
26
  event = possible_render_event(props:)
26
27
 
27
- # TODO: Get LowLoad to load constants defined in "<{ MyNode }>" syntax so that we can resolve namespace/params on class load.
28
28
  klass = class_constant(namespace: @namespace&.split('::') || [], name:)
29
- instance = klass.new(event:)
29
+ keywords = props.merge(event:)
30
+ instance = klass.new(**Keywords.accepted(klass.instance_method(:initialize), keywords))
30
31
 
31
32
  # Classes referenced via "<{ ChildNode }>" must implement class/instance render/render_template methods (See LowNode).
32
- return instance.render_template(event:, parent_binding: current_binding, slot_node: self, props:) if klass.template
33
+ if klass.respond_to?(:template) && klass.template
34
+ return instance.render_template(event:, parent_binding: current_binding, slot_node: self, props:)
35
+ end
36
+
37
+ # Without a class template, a component's own render method can receive its slot's rendered children as slot:.
38
+ render_method = instance.method(:render)
39
+ keywords[:slot] = slot(current_binding:, metadata:) if Keywords.takes?(render_method, :slot)
40
+ instance.render(**Keywords.accepted(render_method, keywords))
41
+ end
33
42
 
34
- props.empty? ? instance.render(event:) : instance.render(event:, **props)
43
+ # The children rendered in the binding the slot is in, like a yield in a template renders them.
44
+ def slot(current_binding:, metadata:)
45
+ @children.map { |child| child.render(current_binding:, parent_binding: current_binding, slot_node: self, metadata:) || '' }.join
35
46
  end
36
47
 
37
48
  def end_name
@@ -7,6 +7,15 @@ module Antlers
7
7
  wrapped_in?(string, "'") || wrapped_in?(string, '"')
8
8
  end
9
9
 
10
+ # A number literal, like 3, -2 or 1.5, rather than a variable or method name.
11
+ def number?(string)
12
+ string.match?(/\A-?\d+(\.\d+)?\z/)
13
+ end
14
+
15
+ def number(string)
16
+ string.include?('.') ? string.to_f : string.to_i
17
+ end
18
+
10
19
  def wrapped_in?(string, delimeter)
11
20
  string[0] == delimeter && string[-1] == delimeter
12
21
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antlers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-13 00:00:00.000000000 Z
10
+ date: 2026-10-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: erb
@@ -62,6 +62,7 @@ files:
62
62
  - lib/antlers.rb
63
63
  - lib/antlers/api.rb
64
64
  - lib/antlers/elements.rb
65
+ - lib/antlers/keywords.rb
65
66
  - lib/antlers/lexer.rb
66
67
  - lib/antlers/parser.rb
67
68
  - lib/antlers/version.rb