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 +4 -4
- data/lib/antlers/keywords.rb +27 -0
- data/lib/antlers/lexer.rb +2 -1
- data/lib/antlers/version.rb +1 -1
- data/lib/antlers.rb +7 -0
- data/lib/interfaces/antler_node.rb +1 -3
- data/lib/modules/namespace.rb +4 -3
- data/lib/modules/props.rb +1 -1
- data/lib/modules/variables.rb +5 -3
- data/lib/nodes/prop_node.rb +28 -22
- data/lib/nodes/slot_node.rb +15 -4
- data/lib/support/queries.rb +9 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f686ef1d7e193fd0f89d1193b2eaca591adeb32b4d3ab4921b8f720b344b4d1c
|
|
4
|
+
data.tar.gz: 39c5d31434df9e67ad4ba47e8f3e3af3598baa61a5c4faffee4645361f3a2d44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/antlers/version.rb
CHANGED
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?(
|
|
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
|
data/lib/modules/namespace.rb
CHANGED
|
@@ -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
|
|
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
data/lib/modules/variables.rb
CHANGED
|
@@ -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.
|
|
13
|
-
# 3.
|
|
14
|
-
# 4. A method
|
|
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('.')
|
data/lib/nodes/prop_node.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
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
|
data/lib/nodes/slot_node.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/support/queries.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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
|