antlers 0.0.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: 1af03cc260e8dd49409f43d325fbf63271128bc55dcb9f9d8fa87151b2e4f3c9
4
- data.tar.gz: f60f7b0ef2eef4c809a90d434b38e3dc20e0a7e2665633909b403433aa1a6726
3
+ metadata.gz: eca914b3e0a81f538aaa1e4db0b477a9b46c0934d365786962d2a391c957b3ee
4
+ data.tar.gz: 7f46ddc34ff748822e99537b59e0300fe6c094abe79d2cd042ad78ad7cb9068d
5
5
  SHA512:
6
- metadata.gz: 1f7d7f8a0a141c2cb9bb8ccd4907b64d69a9d75cbf9fe98dfb8ddc4f3c37a175f38ae3e7dfb7798a9b2bcb07dd9abb7d537e472b62e534daeb7635a70bdcabc9
7
- data.tar.gz: d333bc441ccf5531e22325c94ccffe0820215ce68be6bd3a4c512fe8e28c5fd5ff711e18fc81ec0a2336fff131cc281b2b348d6f893ce6f6407c5cb4e75ba424
6
+ metadata.gz: ca62292741c30d4a3ed0a0c0b62dcb20a895eb3018d55b179ef3512f47e078628eff04089b285490add876d1cfa5ca473a4b85b7bdf01d69d56e459b0364e1bb
7
+ data.tar.gz: db84676dac56bce48b73b1019d73924c7c82c7e4c98d6b5a9c9dddbae2e8defd68336f8ec83e64154403a2f9d1fe611d441ebec99f5f2c2a899bb2fe6a7acfc4
data/lib/antlers.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lexer'
4
+ require_relative 'parser'
5
+
6
+ require 'low_event'
7
+
8
+ module Antlers
9
+ class << self
10
+ def parse(template)
11
+ return template unless template.include?('<{') || template.include?('{')
12
+
13
+ lexemes = Lexer.new.parse(template)
14
+ Parser.parse(lexemes)
15
+ end
16
+
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
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../nodes/prop_node'
4
+ require_relative '../nodes/slot_node'
5
+ require_relative '../nodes/var_node'
6
+ require_relative '../nodes/yield_node'
7
+
8
+ module Antlers
9
+ class NodeFactory
10
+ class << self
11
+ def prop_node(segment:)
12
+ PropNode.new(name: segment[:prop], props: segment[:props])
13
+ end
14
+
15
+ def slot_node(segment:)
16
+ SlotNode.new(name: segment[:slot_def], props: segment[:props])
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
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antlers
4
+ class AntlerNode
5
+ attr_reader :name
6
+
7
+ def initialize(name:)
8
+ @name = name
9
+ end
10
+
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.
16
+ def ==(other) = other.class == self.class
17
+ def eql?(other) = self == other
18
+ def hash = [self.class].hash
19
+ end
20
+ end
@@ -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 ADDED
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'queries'
4
+
5
+ module Antlers
6
+ extend Queries
7
+
8
+ class LexerParseError < StandardError; end
9
+
10
+ class Lexer
11
+ def initialize
12
+ @delimiters = ['<{', '}>', '{', '}']
13
+ @keywords = ['if:', 'for:', 'in:', 'slot:', ':slot']
14
+ @cursor = 0
15
+ end
16
+
17
+ def parse(template)
18
+ @cursor = 0
19
+ sequence = []
20
+
21
+ # Split on delimiters and retain capture groups.
22
+ segments = template.split(/(#{Regexp.union(@delimiters)})/).map(&:strip)
23
+
24
+ until segments[@cursor].nil?
25
+ if (antlers_segment = antlers_segment(segments:))
26
+ sequence << antlers_lexeme(antlers_segment:, segments:)
27
+ # Skipping: ['{', 'expression', '}']
28
+ # Skipping: ['<{', 'name + props + keywords', '}>']
29
+ @cursor += 3
30
+ else
31
+ segment = segments[@cursor]
32
+ sequence << segment unless segment.empty?
33
+ @cursor += 1
34
+ end
35
+ end
36
+
37
+ sequence
38
+ end
39
+
40
+ private
41
+
42
+ def antlers_segment(segments:)
43
+ next_segment = segments[@cursor + 1]
44
+ return nil unless next_segment && (segments[@cursor] == '<{' || var?(segments:))
45
+
46
+ next_segment
47
+ end
48
+
49
+ def antlers_lexeme(antlers_segment:, segments:)
50
+ return var(antlers_segment:) if var?(segments:)
51
+
52
+ name, props, keywords = parse_segment(antlers_segment:)
53
+
54
+ return slot_yield if slot_yield?(keywords)
55
+ return slot(name:, props:) if slot?(name)
56
+ return prop(name:, props:) if prop?(name)
57
+
58
+ raise LexerParseError, "Couldn't parse antlers syntax: '#{antlers_segment}'"
59
+ end
60
+
61
+ def parse_segment(antlers_segment:)
62
+ name_and_props, *keywords = antlers_segment.split(/(#{Regexp.union(@keywords)})/)
63
+ name, *props = name_and_props.split(' ')
64
+
65
+ [name, props, keywords]
66
+ end
67
+
68
+ def var?(segments:)
69
+ first, middle, last = segments[@cursor..@cursor + 3].map(&:strip)
70
+ first == '{' && last == '}'
71
+ end
72
+
73
+ def slot?(name)
74
+ name && (name.start_with?(':') || name.end_with?(':'))
75
+ end
76
+
77
+ def slot_yield?(keywords)
78
+ keywords.include?(':slot')
79
+ end
80
+
81
+ def prop?(name)
82
+ name && [*'A'..'Z'].include?(name[0])
83
+ end
84
+
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 }
92
+ end
93
+
94
+ def slot(name:, props:)
95
+ if name.end_with?(':')
96
+ slot_def = { slot_def: name.delete_suffix(':') }
97
+ slot_def[:props] = props(props) unless props.empty?
98
+ return slot_def
99
+ end
100
+
101
+ { slot_end: name.delete_prefix(':') }
102
+ end
103
+
104
+ def slot_yield
105
+ { slot: :default }
106
+ end
107
+
108
+ def prop(name:, props:)
109
+ prop = { prop: name }
110
+ prop[:props] = props(props) unless props.empty?
111
+ prop
112
+ end
113
+
114
+ def props(props)
115
+ odd_props = props.join(' ').split(/(=)|\s/)
116
+
117
+ return {} unless odd_props.any?
118
+
119
+ props = {}
120
+ until odd_props.empty?
121
+ prop = odd_props.shift
122
+ value = nil
123
+
124
+ if odd_props.first == '='
125
+ odd_props.shift
126
+ value = odd_props.shift
127
+ end
128
+
129
+ props[prop.to_sym] = value
130
+ end
131
+
132
+ props
133
+ end
134
+ end
135
+ end
@@ -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
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/leaf_node'
4
+ require_relative '../modules/props'
5
+
6
+ module Antlers
7
+ class PropNode < LeafNode
8
+ include Props
9
+
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:)
13
+
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)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/branch_node'
4
+
5
+ module Antlers
6
+ class RootNode < BranchNode
7
+ def initialize(name: :root_node, children: [])
8
+ super(name:, children:)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/branch_node'
4
+ require_relative '../modules/props'
5
+
6
+ module Antlers
7
+ class SlotNode < BranchNode
8
+ include Props
9
+
10
+ attr_accessor :children
11
+
12
+ def initialize(name:, props: [], children: [])
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
25
+
26
+ props.empty? ? instance.render(event:) : instance.render(event:, **props)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ require_relative '../interfaces/leaf_node'
6
+
7
+ module Antlers
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
39
+ end
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 ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'factories/node_factory'
4
+ require_relative 'nodes/root_node'
5
+
6
+ module Antlers
7
+ module Parser
8
+ class << self
9
+ def parse(sequence, id: :root_node)
10
+ branch(branch_node: RootNode.new(name: id), sequence:)
11
+ end
12
+
13
+ def branch(branch_node:, sequence:) # rubocop:disable Metrics/AbcSize
14
+ until sequence.empty?
15
+ segment = sequence.shift
16
+
17
+ if segment.is_a?(String)
18
+ branch_node.children << segment
19
+ elsif segment[:var]
20
+ branch_node.children << NodeFactory.var_node(segment:)
21
+ elsif segment[:prop]
22
+ branch_node.children << NodeFactory.prop_node(segment:)
23
+ elsif segment[:slot]
24
+ branch_node.children << NodeFactory.yield_node(segment:)
25
+ elsif segment[:slot_def]
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)
33
+ end
34
+ end
35
+
36
+ branch_node
37
+ end
38
+ end
39
+ end
40
+ 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.0.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.0.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
@@ -30,6 +44,20 @@ executables: []
30
44
  extensions: []
31
45
  extra_rdoc_files: []
32
46
  files:
47
+ - lib/antlers.rb
48
+ - lib/factories/node_factory.rb
49
+ - lib/interfaces/antler_node.rb
50
+ - lib/interfaces/branch_node.rb
51
+ - lib/interfaces/leaf_node.rb
52
+ - lib/lexer.rb
53
+ - lib/modules/props.rb
54
+ - lib/nodes/prop_node.rb
55
+ - lib/nodes/root_node.rb
56
+ - lib/nodes/slot_node.rb
57
+ - lib/nodes/var_node.rb
58
+ - lib/nodes/yield_node.rb
59
+ - lib/parser.rb
60
+ - lib/queries.rb
33
61
  - lib/version.rb
34
62
  homepage: https://codeberg.org/raindeer/antlers
35
63
  licenses: []