antlers 0.1.0 → 0.2.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: 7aef5ead7628092869dbae7f4ae7fee7bea88a69e1c3ee2e9ebd5fd6ace600ea
4
- data.tar.gz: a96ba7818b542305105305c9c7cbc73a6c4962b118e6f0686f1eb73cfda59a56
3
+ metadata.gz: eca914b3e0a81f538aaa1e4db0b477a9b46c0934d365786962d2a391c957b3ee
4
+ data.tar.gz: 7f46ddc34ff748822e99537b59e0300fe6c094abe79d2cd042ad78ad7cb9068d
5
5
  SHA512:
6
- metadata.gz: ff1db9dd204501ae40f3ccc43ebf56b236aa1776ef233edd7a982d34ca5ad5d5486aaad29de46f6f382adc092cb54d5169a6a84443faae5799eb26cb7a9c96ee
7
- data.tar.gz: 5637d1f3cb287c70b7f47d0cb7405c594b98fc3858a624e10302ef935e88872f07fa5a67403bbb091a7ec29af758161f8effa9c2055ddd3a12ec2aa32d1d470a
6
+ metadata.gz: ca62292741c30d4a3ed0a0c0b62dcb20a895eb3018d55b179ef3512f47e078628eff04089b285490add876d1cfa5ca473a4b85b7bdf01d69d56e459b0364e1bb
7
+ data.tar.gz: db84676dac56bce48b73b1019d73924c7c82c7e4c98d6b5a9c9dddbae2e8defd68336f8ec83e64154403a2f9d1fe611d441ebec99f5f2c2a899bb2fe6a7acfc4
data/lib/antlers.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'lexer'
4
+ require_relative 'parser'
5
+
6
+ require 'low_event'
7
+
3
8
  module Antlers
4
9
  class << self
5
10
  def parse(template)
@@ -9,7 +14,8 @@ module Antlers
9
14
  Parser.parse(lexemes)
10
15
  end
11
16
 
12
- # TODO: Render the AST.
13
- def render(antler_node); end
17
+ def render(ast:, current_binding:, parent_binding: nil, slot_node: nil, namespace: nil)
18
+ ast.render(current_binding:, parent_binding:, slot_node:, namespace:)
19
+ end
14
20
  end
15
21
  end
@@ -3,14 +3,11 @@
3
3
  require_relative '../nodes/prop_node'
4
4
  require_relative '../nodes/slot_node'
5
5
  require_relative '../nodes/var_node'
6
+ require_relative '../nodes/yield_node'
6
7
 
7
8
  module Antlers
8
9
  class NodeFactory
9
10
  class << self
10
- def var_node(segment:)
11
- VarNode.new(name: segment[:ivar])
12
- end
13
-
14
11
  def prop_node(segment:)
15
12
  PropNode.new(name: segment[:prop], props: segment[:props])
16
13
  end
@@ -18,6 +15,14 @@ module Antlers
18
15
  def slot_node(segment:)
19
16
  SlotNode.new(name: segment[:slot_def], props: segment[:props])
20
17
  end
18
+
19
+ def var_node(segment:)
20
+ VarNode.new(value: segment[:var])
21
+ end
22
+
23
+ def yield_node(segment:)
24
+ YieldNode.new(name: segment[:slot])
25
+ end
21
26
  end
22
27
  end
23
28
  end
@@ -8,7 +8,11 @@ module Antlers
8
8
  @name = name
9
9
  end
10
10
 
11
- # Consider this a value object on comparison.
11
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ # Consider instance a value object on comparison.
12
16
  def ==(other) = other.class == self.class
13
17
  def eql?(other) = self == other
14
18
  def hash = [self.class].hash
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'antler_node'
4
+
5
+ module Antlers
6
+ class BranchNode < AntlerNode
7
+ attr_accessor :children
8
+
9
+ def initialize(name:, children: [])
10
+ super(name:)
11
+
12
+ @children = children
13
+ end
14
+
15
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
16
+ output = ''
17
+
18
+ @children.each do |child|
19
+ # Antlers nodes respond to "render", whereas HTML is stored as a string and output as is.
20
+ output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:, namespace:) : child) || ''
21
+ end
22
+
23
+ output
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'antler_node'
4
+
5
+ module Antlers
6
+ class LeafNode < AntlerNode
7
+ end
8
+ end
data/lib/lexer.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'queries'
4
+
3
5
  module Antlers
6
+ extend Queries
7
+
4
8
  class LexerParseError < StandardError; end
5
9
 
6
10
  class Lexer
7
11
  def initialize
8
12
  @delimiters = ['<{', '}>', '{', '}']
9
- @keywords = ['if:', 'for:', 'in:']
13
+ @keywords = ['if:', 'for:', 'in:', 'slot:', ':slot']
10
14
  @cursor = 0
11
15
  end
12
16
 
@@ -20,7 +24,7 @@ module Antlers
20
24
  until segments[@cursor].nil?
21
25
  if (antlers_segment = antlers_segment(segments:))
22
26
  sequence << antlers_lexeme(antlers_segment:, segments:)
23
- # Skipping: ['{', '@ivar', '}']
27
+ # Skipping: ['{', 'expression', '}']
24
28
  # Skipping: ['<{', 'name + props + keywords', '}>']
25
29
  @cursor += 3
26
30
  else
@@ -37,16 +41,17 @@ module Antlers
37
41
 
38
42
  def antlers_segment(segments:)
39
43
  next_segment = segments[@cursor + 1]
40
- return nil unless next_segment && (segments[@cursor] == '<{' || ivar?(segments:))
44
+ return nil unless next_segment && (segments[@cursor] == '<{' || var?(segments:))
41
45
 
42
46
  next_segment
43
47
  end
44
48
 
45
49
  def antlers_lexeme(antlers_segment:, segments:)
46
- return ivar(antlers_segment:) if ivar?(segments:)
50
+ return var(antlers_segment:) if var?(segments:)
47
51
 
48
- name, props, _keywords = parse_segment(antlers_segment:)
52
+ name, props, keywords = parse_segment(antlers_segment:)
49
53
 
54
+ return slot_yield if slot_yield?(keywords)
50
55
  return slot(name:, props:) if slot?(name)
51
56
  return prop(name:, props:) if prop?(name)
52
57
 
@@ -60,21 +65,30 @@ module Antlers
60
65
  [name, props, keywords]
61
66
  end
62
67
 
63
- def ivar?(segments:)
64
- first, second, third = segments[@cursor..@cursor + 3].map(&:strip)
65
- first == '{' && second&.start_with?('@') && third == '}'
68
+ def var?(segments:)
69
+ first, middle, last = segments[@cursor..@cursor + 3].map(&:strip)
70
+ first == '{' && last == '}'
66
71
  end
67
72
 
68
73
  def slot?(name)
69
- name.start_with?(':') || name.end_with?(':')
74
+ name && (name.start_with?(':') || name.end_with?(':'))
75
+ end
76
+
77
+ def slot_yield?(keywords)
78
+ keywords.include?(':slot')
70
79
  end
71
80
 
72
81
  def prop?(name)
73
- [*'A'..'Z'].include?(name[0])
82
+ name && [*'A'..'Z'].include?(name[0])
74
83
  end
75
84
 
76
- def ivar(antlers_segment:)
77
- { ivar: antlers_segment.delete_prefix('@') }
85
+ def var(antlers_segment:)
86
+ # 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
90
+
91
+ { var: antlers_segment }
78
92
  end
79
93
 
80
94
  def slot(name:, props:)
@@ -87,6 +101,10 @@ module Antlers
87
101
  { slot_end: name.delete_prefix(':') }
88
102
  end
89
103
 
104
+ def slot_yield
105
+ { slot: :default }
106
+ end
107
+
90
108
  def prop(name:, props:)
91
109
  prop = { prop: name }
92
110
  prop[:props] = props(props) unless props.empty?
@@ -108,7 +126,7 @@ module Antlers
108
126
  value = odd_props.shift
109
127
  end
110
128
 
111
- props[prop] = value
129
+ props[prop.to_sym] = value
112
130
  end
113
131
 
114
132
  props
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antlers
4
+ module Props
5
+ attr_accessor :props
6
+
7
+ def initialize(name:, props: {}, **)
8
+ super(name:, **)
9
+
10
+ @props = props
11
+ end
12
+
13
+ private
14
+
15
+ def create_event(props:)
16
+ Low::Events::RenderEvent.new(action: :render, props:)
17
+ end
18
+
19
+ def evaluate_props(props:, current_binding:)
20
+ return {} if props.nil?
21
+
22
+ evaluated_props = {}
23
+
24
+ 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
32
+ end
33
+
34
+ evaluated_props
35
+ end
36
+
37
+ def class_from_namespace(namespace:, name:)
38
+ return Object.const_get(name) if Object.const_defined?(name) || name.start_with?('::') || namespace.empty?
39
+
40
+ namespace_with_name = [namespace, name].join('::')
41
+ return Object.const_get(namespace_with_name) if Object.const_defined?(namespace_with_name)
42
+
43
+ namespace.pop
44
+ class_from_namespace(namespace:, name:)
45
+ end
46
+ end
47
+ end
@@ -1,15 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/antler_node'
3
+ require_relative '../interfaces/leaf_node'
4
+ require_relative '../modules/props'
4
5
 
5
6
  module Antlers
6
- attr_accessor :props
7
+ class PropNode < LeafNode
8
+ include Props
7
9
 
8
- class PropNode < AntlerNode
9
- def initialize(name:, props: [])
10
- super(name:)
10
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
11
+ props = evaluate_props(props: @props, current_binding:)
12
+ event = create_event(props:)
11
13
 
12
- @props = props
14
+ klass = class_from_namespace(namespace: namespace&.split('::') || [], name: @name)
15
+ instance = klass.new(event:)
16
+
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
19
+
20
+ props.empty? ? instance.render(event:) : instance.render(event:, **props)
13
21
  end
14
22
  end
15
23
  end
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/antler_node'
3
+ require_relative '../interfaces/branch_node'
4
4
 
5
5
  module Antlers
6
- class RootNode < AntlerNode
7
- attr_accessor :children
8
-
6
+ class RootNode < BranchNode
9
7
  def initialize(name: :root_node, children: [])
10
- super(name:)
11
-
12
- @children = children
8
+ super(name:, children:)
13
9
  end
14
10
  end
15
11
  end
@@ -1,15 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../nodes/prop_node'
3
+ require_relative '../interfaces/branch_node'
4
+ require_relative '../modules/props'
4
5
 
5
6
  module Antlers
6
- class SlotNode < PropNode
7
+ class SlotNode < BranchNode
8
+ include Props
9
+
7
10
  attr_accessor :children
8
11
 
9
12
  def initialize(name:, props: [], children: [])
10
- super(name:, props:)
13
+ super(name:, props:, children:)
14
+ end
15
+
16
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
17
+ props = evaluate_props(props: @props, current_binding:)
18
+ event = create_event(props:)
19
+
20
+ klass = class_from_namespace(namespace: namespace&.split('::') || [], name: @name)
21
+ instance = klass.new(event:)
22
+
23
+ # Classes referenced via "<{ ChildNode }>" must implement class/instance render/render_template methods (See LowNode).
24
+ return instance.render_template(event:, parent_binding: current_binding, slot_node: self, props:) if klass.template
11
25
 
12
- @children = children
26
+ props.empty? ? instance.render(event:) : instance.render(event:, **props)
13
27
  end
14
28
  end
15
29
  end
@@ -1,8 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/antler_node'
3
+ require 'erb'
4
+
5
+ require_relative '../interfaces/leaf_node'
4
6
 
5
7
  module Antlers
6
- class VarNode < AntlerNode
8
+ class VarNode < LeafNode
9
+ attr_reader :value
10
+
11
+ def initialize(name: :var, value:)
12
+ super(name:)
13
+
14
+ @value = value
15
+ end
16
+
17
+ 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
38
+ end
7
39
  end
8
40
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/leaf_node'
4
+
5
+ module Antlers
6
+ class YieldNode < BranchNode
7
+ def initialize(name: :default)
8
+ super(name:)
9
+ end
10
+
11
+ # Renders the children of the parent node in the binding of the parent.
12
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
13
+ output = ''
14
+
15
+ slot_node.children.each do |child|
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) || ''
18
+ end
19
+
20
+ output
21
+ end
22
+ end
23
+ end
data/lib/parser.rb CHANGED
@@ -16,16 +16,18 @@ module Antlers
16
16
 
17
17
  if segment.is_a?(String)
18
18
  branch_node.children << segment
19
- elsif segment[:ivar]
19
+ elsif segment[:var]
20
20
  branch_node.children << NodeFactory.var_node(segment:)
21
21
  elsif segment[:prop]
22
22
  branch_node.children << NodeFactory.prop_node(segment:)
23
+ elsif segment[:slot]
24
+ branch_node.children << NodeFactory.yield_node(segment:)
23
25
  elsif segment[:slot_def]
24
26
  slot_node = NodeFactory.slot_node(segment:)
25
27
  branch_node.children << slot_node
26
28
 
27
29
  sub_sequence = []
28
- sub_sequence << sequence.shift until sequence.first[:slot_end] == slot_node.name
30
+ sub_sequence << sequence.shift until sequence.first.is_a?(Hash) && sequence.first[:slot_end] == slot_node.name
29
31
 
30
32
  branch(branch_node: slot_node, sequence: sub_sequence)
31
33
  end
data/lib/queries.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antlers
4
+ module Queries
5
+ class << self
6
+ def user_defined_string?(string)
7
+ wrapped_in?(string, %q{'}) || wrapped_in?(string, %q{"})
8
+ end
9
+
10
+ def wrapped_in?(string, delimeter)
11
+ string[0] == delimeter && string[-1] == delimeter
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antlers
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -10,19 +10,33 @@ 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_type
13
+ name: low_event
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '1.0'
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: erb
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  description: A new Ruby templating language. Coming soon...
27
41
  email:
28
42
  - maediprichard@gmail.com
@@ -33,12 +47,17 @@ files:
33
47
  - lib/antlers.rb
34
48
  - lib/factories/node_factory.rb
35
49
  - lib/interfaces/antler_node.rb
50
+ - lib/interfaces/branch_node.rb
51
+ - lib/interfaces/leaf_node.rb
36
52
  - lib/lexer.rb
53
+ - lib/modules/props.rb
37
54
  - lib/nodes/prop_node.rb
38
55
  - lib/nodes/root_node.rb
39
56
  - lib/nodes/slot_node.rb
40
57
  - lib/nodes/var_node.rb
58
+ - lib/nodes/yield_node.rb
41
59
  - lib/parser.rb
60
+ - lib/queries.rb
42
61
  - lib/version.rb
43
62
  homepage: https://codeberg.org/raindeer/antlers
44
63
  licenses: []