antlers 0.2.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eca914b3e0a81f538aaa1e4db0b477a9b46c0934d365786962d2a391c957b3ee
4
- data.tar.gz: 7f46ddc34ff748822e99537b59e0300fe6c094abe79d2cd042ad78ad7cb9068d
3
+ metadata.gz: 581b3e4990e2c5279c26989e05e545f61a80a3fb8590c8a4bfd058878f2684f8
4
+ data.tar.gz: 5135d9c10a19fa9bb80c79694f998287c1dad614514a6d22c915cfc371d54b71
5
5
  SHA512:
6
- metadata.gz: ca62292741c30d4a3ed0a0c0b62dcb20a895eb3018d55b179ef3512f47e078628eff04089b285490add876d1cfa5ca473a4b85b7bdf01d69d56e459b0364e1bb
7
- data.tar.gz: db84676dac56bce48b73b1019d73924c7c82c7e4c98d6b5a9c9dddbae2e8defd68336f8ec83e64154403a2f9d1fe611d441ebec99f5f2c2a899bb2fe6a7acfc4
6
+ metadata.gz: 4e85dd8fc504c21fe4c71f692203dc695fd60834e2f2f33185c1daf6edbf3e64c4c8eccf29710d899f5beab1c348cac1c9fe6a8350778b7563cd4d879cef78e2
7
+ data.tar.gz: 5b4853ba5e0d0749d4305c57923d6488ddcd5b05892aa2e274dd3683519ae85fcbd9a84b117e9c453a9b1ab256c814cd6f6d35b71db76935e30f0c86d17852ba
data/lib/antlers.rb CHANGED
@@ -3,11 +3,9 @@
3
3
  require_relative 'lexer'
4
4
  require_relative 'parser'
5
5
 
6
- require 'low_event'
7
-
8
6
  module Antlers
9
7
  class << self
10
- def parse(template)
8
+ def ast(template)
11
9
  return template unless template.include?('<{') || template.include?('{')
12
10
 
13
11
  lexemes = Lexer.new.parse(template)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../nodes/for_node'
3
4
  require_relative '../nodes/prop_node'
4
5
  require_relative '../nodes/slot_node'
5
6
  require_relative '../nodes/var_node'
@@ -8,6 +9,11 @@ require_relative '../nodes/yield_node'
8
9
  module Antlers
9
10
  class NodeFactory
10
11
  class << self
12
+ def for_node(segment:)
13
+ value, key, items = segment.values_at(:for_def, :key, :in)
14
+ ForNode.new(name: value, key:, value:, items:)
15
+ end
16
+
11
17
  def prop_node(segment:)
12
18
  PropNode.new(name: segment[:prop], props: segment[:props])
13
19
  end
data/lib/lexer.rb CHANGED
@@ -10,7 +10,7 @@ module Antlers
10
10
  class Lexer
11
11
  def initialize
12
12
  @delimiters = ['<{', '}>', '{', '}']
13
- @keywords = ['if:', 'for:', 'in:', 'slot:', ':slot']
13
+ @keywords = ['if:', 'for:', 'in:', ':for', 'slot:', ':slot']
14
14
  @cursor = 0
15
15
  end
16
16
 
@@ -54,22 +54,27 @@ module Antlers
54
54
  return slot_yield if slot_yield?(keywords)
55
55
  return slot(name:, props:) if slot?(name)
56
56
  return prop(name:, props:) if prop?(name)
57
+ return for_loop(keywords:) if for_loop?(keywords:)
57
58
 
58
- raise LexerParseError, "Couldn't parse antlers syntax: '#{antlers_segment}'"
59
+ raise LexerParseError, "Unrecognised syntax: '#{antlers_segment}'"
59
60
  end
60
61
 
61
62
  def parse_segment(antlers_segment:)
62
63
  name_and_props, *keywords = antlers_segment.split(/(#{Regexp.union(@keywords)})/)
63
64
  name, *props = name_and_props.split(' ')
64
65
 
65
- [name, props, keywords]
66
+ [name, props, keywords.map(&:strip)]
66
67
  end
67
68
 
68
69
  def var?(segments:)
69
- first, middle, last = segments[@cursor..@cursor + 3].map(&:strip)
70
+ first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
70
71
  first == '{' && last == '}'
71
72
  end
72
73
 
74
+ def for_loop?(keywords:)
75
+ ['for:', ':for'].include?(keywords.first)
76
+ end
77
+
73
78
  def slot?(name)
74
79
  name && (name.start_with?(':') || name.end_with?(':'))
75
80
  end
@@ -84,13 +89,25 @@ module Antlers
84
89
 
85
90
  def var(antlers_segment:)
86
91
  # String is already interpolated or not depending on user input on the template layer, now we store it without those template quotes.
87
- if Queries.user_defined_string?(antlers_segment)
88
- antlers_segment = antlers_segment[1..-2]
89
- end
92
+ antlers_segment = antlers_segment[1..-2] if Queries.user_defined_string?(antlers_segment)
90
93
 
91
94
  { var: antlers_segment }
92
95
  end
93
96
 
97
+ def for_loop(keywords:)
98
+ key_values = keywords.count.even? ? keywords.each_slice(2).to_h : {}
99
+
100
+ if key_values['for:']
101
+ *key, value = key_values['for:'].split(',').map(&:strip)
102
+ for_def = { for_def: value, in: key_values['in:'] }
103
+ for_def[:key] = key.first unless key.empty?
104
+ return for_def
105
+ end
106
+
107
+ # TODO: Keep track of which for loop we're in to allow nested for loops.
108
+ { for_end: 'level_1' }
109
+ end
110
+
94
111
  def slot(name:, props:)
95
112
  if name.end_with?(':')
96
113
  slot_def = { slot_def: name.delete_suffix(':') }
data/lib/modules/props.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'low_event'
4
+ require_relative 'variables'
5
+
3
6
  module Antlers
4
7
  module Props
8
+ include Variables
9
+
5
10
  attr_accessor :props
6
11
 
7
12
  def initialize(name:, props: {}, **)
@@ -12,7 +17,7 @@ module Antlers
12
17
 
13
18
  private
14
19
 
15
- def create_event(props:)
20
+ def create_render_event(props:)
16
21
  Low::Events::RenderEvent.new(action: :render, props:)
17
22
  end
18
23
 
@@ -22,13 +27,7 @@ module Antlers
22
27
  evaluated_props = {}
23
28
 
24
29
  props.each do |name, value|
25
- receiver = current_binding.receiver
26
-
27
- if receiver.respond_to?(value.to_sym)
28
- evaluated_props[name] = receiver.send(value.to_sym)
29
- elsif value.start_with?('@')
30
- evaluated_props[name] = receiver.instance_variable_get(value)
31
- end
30
+ evaluated_props[name] = evaluate(name: value, current_binding:)
32
31
  end
33
32
 
34
33
  evaluated_props
@@ -0,0 +1,38 @@
1
+ module Antlers
2
+ module Variables
3
+ # Evaluation limited to the following for security and to prevent a templating language becoming a programming language.
4
+ # 1. An instance variable
5
+ # 2. A method call/local variable
6
+ # 3. A method chain
7
+ # 4. A static string
8
+ def evaluate(name:, current_binding:)
9
+ return @value.to_s unless current_binding
10
+
11
+ name, *chain = name.split('.')
12
+
13
+ result = method_var(name:, current_binding:)
14
+ return method_chain(result:, chain:, current_binding:) if chain.count > 0
15
+ return result if result
16
+
17
+ @value.to_s
18
+ rescue NameError
19
+ @value.to_s
20
+ end
21
+
22
+ private
23
+
24
+ def method_var(name:, current_binding:)
25
+ return current_binding.receiver.instance_variable_get(name) if name.start_with?('@')
26
+ return current_binding.local_variable_get(name) if current_binding.local_variable_defined?(name)
27
+ return current_binding.receiver.send(name.to_sym) if current_binding.receiver.respond_to?(name.to_sym)
28
+
29
+ nil
30
+ end
31
+
32
+ def method_chain(result:, chain:, current_binding:)
33
+ chain.reduce(result) do |result, method_call|
34
+ result.send(method_call.to_sym)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/branch_node'
4
+ require_relative '../modules/props'
5
+ require_relative '../modules/variables'
6
+
7
+ module Antlers
8
+ class ForNode < BranchNode
9
+ include Props
10
+ include Variables
11
+
12
+ attr_accessor :children
13
+
14
+ def initialize(name:, items:, value:, key: nil, props: [], children: [])
15
+ super(name:, props:, children:)
16
+
17
+ @items = items
18
+ @value = value
19
+ @key = key
20
+ end
21
+
22
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
23
+ output = ''
24
+
25
+ evaluate(name: @items, current_binding:).each do |value|
26
+ key, value = value if @key
27
+
28
+ # TODO: Parallelize by creating new bindings and ensuring children have any args they need via RenderEvent.
29
+ current_binding.local_variable_set(@value, value)
30
+ current_binding.local_variable_set(@key, key) if @key
31
+
32
+ @children.each do |child|
33
+ # Antlers nodes respond to "render", whereas HTML is stored as a string and output as is.
34
+ output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:, namespace:) : child) || ''
35
+ end
36
+ end
37
+
38
+ output
39
+ end
40
+ end
41
+ end
@@ -9,15 +9,15 @@ module Antlers
9
9
 
10
10
  def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
11
11
  props = evaluate_props(props: @props, current_binding:)
12
- event = create_event(props:)
12
+ event = create_render_event(props:)
13
13
 
14
- klass = class_from_namespace(namespace: namespace&.split('::') || [], name: @name)
15
- instance = klass.new(event:)
14
+ renderable_klass = class_from_namespace(namespace: namespace&.split('::') || [], name: @name)
15
+ renderable_instance = renderable_klass.new(event:)
16
16
 
17
17
  # Classes referenced via "<{ ChildNode }>" must implement class/instance render/render_template methods (See LowNode).
18
- return instance.render_template(event:, parent_binding:, props:) if klass.template
18
+ return renderable_instance.render_template(event:, parent_binding:, props:) if renderable_klass.template
19
19
 
20
- props.empty? ? instance.render(event:) : instance.render(event:, **props)
20
+ props.empty? ? renderable_instance.render(event:) : renderable_instance.render(event:, **props)
21
21
  end
22
22
  end
23
23
  end
@@ -15,7 +15,7 @@ module Antlers
15
15
 
16
16
  def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
17
17
  props = evaluate_props(props: @props, current_binding:)
18
- event = create_event(props:)
18
+ event = create_render_event(props:)
19
19
 
20
20
  klass = class_from_namespace(namespace: namespace&.split('::') || [], name: @name)
21
21
  instance = klass.new(event:)
@@ -3,38 +3,22 @@
3
3
  require 'erb'
4
4
 
5
5
  require_relative '../interfaces/leaf_node'
6
+ require_relative '../modules/variables'
6
7
 
7
8
  module Antlers
8
9
  class VarNode < LeafNode
10
+ include Variables
11
+
9
12
  attr_reader :value
10
13
 
11
- def initialize(name: :var, value:)
14
+ def initialize(value:, name: :var)
12
15
  super(name:)
13
16
 
14
17
  @value = value
15
18
  end
16
19
 
17
20
  def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
18
- ERB::Util.html_escape(evaluate_value(current_binding))
19
- end
20
-
21
- private
22
-
23
- # A variable is deliberately limited in what it can represent.
24
- # 1. An instance/local variable
25
- # 2. A method call
26
- # 3. A static string
27
- def evaluate_value(current_binding)
28
- if current_binding
29
- return current_binding.receiver.instance_variable_get(@value) if @value.start_with?('@')
30
- return current_binding.local_variable_get(@value) if current_binding.local_variable_defined?(@value)
31
- return current_binding.receiver.send(@value.to_sym) if current_binding.receiver.respond_to?(@value.to_sym)
32
- end
33
-
34
- @value
35
- rescue NameError => e
36
- # TODO: Must be a better way to handle variables input as literal strings.
37
- @value
21
+ ERB::Util.html_escape(evaluate(name: @value, current_binding:) || @value)
38
22
  end
39
23
  end
40
24
  end
@@ -14,7 +14,11 @@ module Antlers
14
14
 
15
15
  slot_node.children.each do |child|
16
16
  # Antlers nodes respond to "render", whereas HTML is stored as a string and output as is.
17
- output += (child.respond_to?(:render) ? child.render(current_binding: parent_binding, parent_binding: nil, slot_node: nil, namespace:) : child) || ''
17
+ if child.respond_to?(:render) # rubocop:disable Style/ConditionalAssignment
18
+ output += child.render(current_binding: parent_binding, parent_binding: nil, slot_node: nil, namespace:)
19
+ else
20
+ output += child || ''
21
+ end
18
22
  end
19
23
 
20
24
  output
data/lib/parser.rb CHANGED
@@ -7,33 +7,40 @@ module Antlers
7
7
  module Parser
8
8
  class << self
9
9
  def parse(sequence, id: :root_node)
10
- branch(branch_node: RootNode.new(name: id), sequence:)
10
+ branch(node: RootNode.new(name: id), sequence:)
11
11
  end
12
12
 
13
- def branch(branch_node:, sequence:) # rubocop:disable Metrics/AbcSize
13
+ def branch(node:, sequence:) # rubocop:disable Metrics/AbcSize
14
14
  until sequence.empty?
15
15
  segment = sequence.shift
16
16
 
17
17
  if segment.is_a?(String)
18
- branch_node.children << segment
18
+ node.children << segment
19
19
  elsif segment[:var]
20
- branch_node.children << NodeFactory.var_node(segment:)
20
+ node.children << NodeFactory.var_node(segment:)
21
21
  elsif segment[:prop]
22
- branch_node.children << NodeFactory.prop_node(segment:)
22
+ node.children << NodeFactory.prop_node(segment:)
23
23
  elsif segment[:slot]
24
- branch_node.children << NodeFactory.yield_node(segment:)
24
+ node.children << NodeFactory.yield_node(segment:)
25
25
  elsif segment[:slot_def]
26
26
  slot_node = NodeFactory.slot_node(segment:)
27
- branch_node.children << slot_node
28
-
29
- sub_sequence = []
30
- sub_sequence << sequence.shift until sequence.first.is_a?(Hash) && sequence.first[:slot_end] == slot_node.name
31
-
32
- branch(branch_node: slot_node, sequence: sub_sequence)
27
+ node.children << slot_node
28
+ sub_branch(node: slot_node, sequence:, end_key: :slot_end, end_name: slot_node.name)
29
+ elsif segment[:for_def]
30
+ for_node = NodeFactory.for_node(segment:)
31
+ node.children << for_node
32
+ sub_branch(node: for_node, sequence:, end_key: :for_end, end_name: 'level_1')
33
33
  end
34
34
  end
35
35
 
36
- branch_node
36
+ node
37
+ end
38
+
39
+ def sub_branch(node:, sequence:, end_key:, end_name: nil)
40
+ sub_sequence = []
41
+ sub_sequence << sequence.shift until sequence.first.is_a?(Hash) && sequence.first[end_key] == end_name
42
+
43
+ branch(node:, sequence: sub_sequence)
37
44
  end
38
45
  end
39
46
  end
data/lib/queries.rb CHANGED
@@ -4,7 +4,7 @@ module Antlers
4
4
  module Queries
5
5
  class << self
6
6
  def user_defined_string?(string)
7
- wrapped_in?(string, %q{'}) || wrapped_in?(string, %q{"})
7
+ wrapped_in?(string, "'") || wrapped_in?(string, '"')
8
8
  end
9
9
 
10
10
  def wrapped_in?(string, delimeter)
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antlers
4
- VERSION = '0.2.0'
4
+ VERSION = '0.4.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antlers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -10,7 +10,7 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: low_event
13
+ name: erb
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
@@ -24,7 +24,7 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: erb
27
+ name: low_event
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
@@ -51,6 +51,8 @@ files:
51
51
  - lib/interfaces/leaf_node.rb
52
52
  - lib/lexer.rb
53
53
  - lib/modules/props.rb
54
+ - lib/modules/variables.rb
55
+ - lib/nodes/for_node.rb
54
56
  - lib/nodes/prop_node.rb
55
57
  - lib/nodes/root_node.rb
56
58
  - lib/nodes/slot_node.rb
@@ -78,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
80
  - !ruby/object:Gem::Version
79
81
  version: '0'
80
82
  requirements: []
81
- rubygems_version: 3.7.2
83
+ rubygems_version: 4.0.10
82
84
  specification_version: 4
83
85
  summary: Deer to be different
84
86
  test_files: []