antlers 0.7.0 → 0.8.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: 4e01a292dd41ce2f99fd7a6bdd5a375d3d2c36a3e4d5280ccaaaf91be9bcde38
4
- data.tar.gz: 84227a375d02da8663bfdf04efaf496be097f7898840d25bc4f02b0c62bc2fff
3
+ metadata.gz: 8d3bcf6e14ade37294793b87b3ab3ab152e519f8c081a130644b0d28d489b63b
4
+ data.tar.gz: d9886a3044fb8654b37f111a9536ba7dfef4945c26fb08112e400ab14e76d8e7
5
5
  SHA512:
6
- metadata.gz: c449e355a550e43aa65e1e00ba482e858b07a1fb0f983325808a538769d6e8eb78ed5dad4a5a3fdd3f24348db7001e59876894b2a98d882e38dd9fa10078104a
7
- data.tar.gz: 5f71032b3da08b3675f52045f513a810bf2228a1c3308ce7dcde2c53596f7525060dbbd34d05ffb1b04cf8672c045a9e117b727f58a972361a32603e53c2d6ff
6
+ metadata.gz: a4179e613ca96ea49fdbc7d991a627a45640182ae5aaa5f01dc935396586ba8b583df469113c26c7a139308956bc18980cdbd04877c73a9b7cd14617682dd2d3
7
+ data.tar.gz: 698769c0d6bfd0e9872c5022f72555290acff3888615f44061ed10847af8f97af58477c35059678c2e02a2bdafc16d3f06ff514ab80aa539abc2de7cbfeda6bd
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../nodes/if_node'
3
4
  require_relative '../nodes/for_node'
4
5
  require_relative '../nodes/form_node'
5
6
  require_relative '../nodes/prop_node'
@@ -13,6 +14,10 @@ module Antlers
13
14
  @namespace = namespace
14
15
  end
15
16
 
17
+ def if_node(segment:)
18
+ IfNode.new(name: segment[:if_def], value: segment[:if_def])
19
+ end
20
+
16
21
  def for_node(segment:)
17
22
  value, key, items = segment.values_at(:for_def, :key, :in)
18
23
  ForNode.new(name: value, key:, value:, items:)
@@ -37,6 +42,10 @@ module Antlers
37
42
  VarNode.new(value: segment[:var])
38
43
  end
39
44
 
45
+ def raw_var_node(segment:)
46
+ VarNode.new(value: segment[:raw_var], raw: true)
47
+ end
48
+
40
49
  def yield_node(segment:)
41
50
  YieldNode.new(name: segment[:slot])
42
51
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antlers
4
+ module Lexeme
5
+ KEYWORDS = []
6
+
7
+ def match?(name: nil, keywords: {}) = false
8
+
9
+ def lexeme(name:, props:, keywords:)
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/lexeme'
4
+
5
+ module Antlers
6
+ module ForLexeme
7
+ include Lexeme
8
+ extend self
9
+
10
+ KEYWORDS = ['for:', 'in:', ':for'].freeze
11
+
12
+ def match?(keywords:, **)
13
+ KEYWORDS.include?(keywords.keys.first)
14
+ end
15
+
16
+ def lexeme(keywords:, **)
17
+ if keywords['for:']
18
+ *key, value = keywords['for:'].split(',').map(&:strip)
19
+ for_def = { for_def: value, in: keywords['in:'] }
20
+ for_def[:key] = key.first unless key.empty?
21
+ return for_def
22
+ end
23
+
24
+ # TODO: Keep track of which for loop we're in to allow nested for loops.
25
+ { for_end: 'level_1' }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/lexeme'
4
+
5
+ module Antlers
6
+ module FormLexeme
7
+ include Lexeme
8
+ extend self
9
+
10
+ KEYWORDS = ['form:', ':form'].freeze
11
+
12
+ def match?(keywords:, **)
13
+ KEYWORDS.include?(keywords.keys.first)
14
+ end
15
+
16
+ def lexeme(keywords:, **)
17
+ if keywords.key?('form:')
18
+ action = keywords['form:'] ? keywords['form:'][1...-1] : nil
19
+ return { form_def: action }
20
+ end
21
+
22
+ { form_end: 'level_1' }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/lexeme'
4
+
5
+ module Antlers
6
+ module IfLexeme
7
+ include Lexeme
8
+ extend self
9
+
10
+ KEYWORDS = ['if:', ':if'].freeze
11
+
12
+ def match?(keywords:, **)
13
+ KEYWORDS.include?(keywords.keys.first)
14
+ end
15
+
16
+ def lexeme(keywords:, **)
17
+ return { if_def: keywords['if:'] } if keywords.key?('if:')
18
+
19
+ { if_end: 'level_1' }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/lexeme'
4
+
5
+ module Antlers
6
+ module PropLexeme
7
+ include Lexeme
8
+ extend self
9
+
10
+ def match?(name:, **)
11
+ name && [*'A'..'Z'].include?(name[0]) && !(name.start_with?(':') || name.end_with?(':'))
12
+ end
13
+
14
+ def lexeme(name:, props:, **)
15
+ prop = { prop: name }
16
+ prop[:props] = props(props) unless props.empty?
17
+ prop
18
+ end
19
+
20
+ private
21
+
22
+ def props(props)
23
+ odd_props = props.join(' ').split(/(=)|\s/)
24
+
25
+ return {} unless odd_props.any?
26
+
27
+ props = {}
28
+ until odd_props.empty?
29
+ prop = odd_props.shift
30
+ value = nil
31
+
32
+ if odd_props.first == '='
33
+ odd_props.shift
34
+ value = odd_props.shift
35
+ end
36
+
37
+ props[prop.to_sym] = value
38
+ end
39
+
40
+ props
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/lexeme'
4
+
5
+ module Antlers
6
+ module SlotLexeme
7
+ include Lexeme
8
+ extend self
9
+
10
+ KEYWORDS = ['slot:', ':slot'].freeze
11
+
12
+ def match?(name:, **)
13
+ name && (name.start_with?(':') || name.end_with?(':'))
14
+ end
15
+
16
+ def lexeme(name:, props:, **)
17
+ if name.end_with?(':')
18
+ slot_def = { slot_def: name.delete_suffix(':') }
19
+ slot_def[:props] = props(props) unless props.empty?
20
+ return slot_def
21
+ end
22
+
23
+ { slot_end: name.delete_prefix(':') }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/lexeme'
4
+
5
+ module Antlers
6
+ module YieldLexeme
7
+ include Lexeme
8
+ extend self
9
+
10
+ def match?(keywords:, **)
11
+ keywords.keys.include?(':slot')
12
+ end
13
+
14
+ def lexeme(**)
15
+ { slot: :default }
16
+ end
17
+ end
18
+ end
data/lib/lexer.rb CHANGED
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'lexemes/for_lexeme'
4
+ require_relative 'lexemes/form_lexeme'
5
+ require_relative 'lexemes/if_lexeme'
6
+ require_relative 'lexemes/prop_lexeme'
7
+ require_relative 'lexemes/slot_lexeme'
8
+ require_relative 'lexemes/yield_lexeme'
3
9
  require_relative 'support/queries'
4
10
 
5
11
  module Antlers
@@ -7,13 +13,20 @@ module Antlers
7
13
 
8
14
  class LexerParseError < StandardError; end
9
15
 
10
- class Lexer
11
- FOR_KEYWORDS = ['for:', 'in:', ':for'].freeze
12
- FORM_KEYWORDS = ['form:', ':form'].freeze
16
+ LEXEMES = [
17
+ # FormLexeme must be ordered before ForLexeme for regex to work.
18
+ Antlers::FormLexeme,
19
+ Antlers::ForLexeme,
20
+ Antlers::IfLexeme,
21
+ Antlers::PropLexeme,
22
+ Antlers::SlotLexeme,
23
+ Antlers::YieldLexeme,
24
+ ]
13
25
 
26
+ class Lexer
14
27
  def initialize
15
28
  @delimiters = ['<{', '}>', '{', '}']
16
- @keywords = ['if:', *FORM_KEYWORDS, *FOR_KEYWORDS, 'slot:', ':slot']
29
+ @keywords = LEXEMES.flat_map { |lexeme| lexeme.const_get(:KEYWORDS) }
17
30
  @cursor = 0
18
31
  end
19
32
 
@@ -44,21 +57,19 @@ module Antlers
44
57
 
45
58
  def antlers_segment(segments:)
46
59
  next_segment = segments[@cursor + 1]
47
- return nil unless next_segment && (segments[@cursor] == '<{' || var?(segments:))
60
+ return nil unless next_segment && (segments[@cursor] == '<{' || brackets?(segments:))
48
61
 
49
62
  next_segment
50
63
  end
51
64
 
52
65
  def antlers_lexeme(antlers_segment:, segments:)
53
- return var(antlers_segment:) if var?(segments:)
66
+ return var(antlers_segment:) if brackets?(segments:)
54
67
 
55
68
  name, props, keywords = parse_segment(antlers_segment:)
56
69
 
57
- return slot_yield if slot_yield?(keywords:)
58
- return slot(name:, props:) if slot?(name)
59
- return prop(name:, props:) if prop?(name)
60
- return for_loop(keywords:) if for_loop?(keywords:)
61
- return form(keywords:) if form?(keywords:)
70
+ lexeme = LEXEMES.find { it.match?(name:, keywords:) }
71
+ return lexeme.lexeme(name:, props:, keywords:) if lexeme
72
+ return var(antlers_segment:, raw: true) if deerheads?(segments:)
62
73
 
63
74
  raise LexerParseError, "Unrecognised syntax: '#{antlers_segment}'"
64
75
  end
@@ -87,98 +98,23 @@ module Antlers
87
98
 
88
99
  # TODO: Refactor every constant, match and result method into its own class. Loop through every class and return the first match.
89
100
 
90
- def var?(segments:)
101
+ def brackets?(segments:)
91
102
  first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
92
103
  first == '{' && last == '}'
93
104
  end
94
105
 
95
- def for_loop?(keywords:)
96
- FOR_KEYWORDS.include?(keywords.keys.first)
97
- end
98
-
99
- def form?(keywords:)
100
- FORM_KEYWORDS.include?(keywords.keys.first)
101
- end
102
-
103
- def slot?(name)
104
- name && (name.start_with?(':') || name.end_with?(':'))
105
- end
106
-
107
- def slot_yield?(keywords:)
108
- keywords.keys.include?(':slot')
109
- end
110
-
111
- def prop?(name)
112
- name && [*'A'..'Z'].include?(name[0])
106
+ def deerheads?(segments:)
107
+ first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
108
+ first == '<{' && last == '}>'
113
109
  end
114
110
 
115
- def var(antlers_segment:)
111
+ def var(antlers_segment:, raw: false)
116
112
  # String is already interpolated or not depending on user input on the template layer, now we store it without those template quotes.
117
113
  antlers_segment = antlers_segment[1..-2] if Queries.user_defined_string?(antlers_segment)
118
114
 
119
- { var: antlers_segment }
120
- end
121
-
122
- def for_loop(keywords:)
123
- if keywords['for:']
124
- *key, value = keywords['for:'].split(',').map(&:strip)
125
- for_def = { for_def: value, in: keywords['in:'] }
126
- for_def[:key] = key.first unless key.empty?
127
- return for_def
128
- end
129
-
130
- # TODO: Keep track of which for loop we're in to allow nested for loops.
131
- { for_end: 'level_1' }
132
- end
133
-
134
- def form(keywords:)
135
- if keywords.key?('form:')
136
- action = keywords['form:'] ? keywords['form:'][1...-1] : nil
137
- return { form_def: action }
138
- end
115
+ return { raw_var: antlers_segment } if raw
139
116
 
140
- { form_end: 'level_1' }
141
- end
142
-
143
- def slot(name:, props:)
144
- if name.end_with?(':')
145
- slot_def = { slot_def: name.delete_suffix(':') }
146
- slot_def[:props] = props(props) unless props.empty?
147
- return slot_def
148
- end
149
-
150
- { slot_end: name.delete_prefix(':') }
151
- end
152
-
153
- def slot_yield
154
- { slot: :default }
155
- end
156
-
157
- def prop(name:, props:)
158
- prop = { prop: name }
159
- prop[:props] = props(props) unless props.empty?
160
- prop
161
- end
162
-
163
- def props(props)
164
- odd_props = props.join(' ').split(/(=)|\s/)
165
-
166
- return {} unless odd_props.any?
167
-
168
- props = {}
169
- until odd_props.empty?
170
- prop = odd_props.shift
171
- value = nil
172
-
173
- if odd_props.first == '='
174
- odd_props.shift
175
- value = odd_props.shift
176
- end
177
-
178
- props[prop.to_sym] = value
179
- end
180
-
181
- props
117
+ { var: antlers_segment }
182
118
  end
183
119
  end
184
120
  end
@@ -0,0 +1,31 @@
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 IfNode < BranchNode
9
+ include Props
10
+ include Variables
11
+
12
+ def initialize(name:, value:, props: [], children: [])
13
+ super(name:, props:, children:)
14
+
15
+ @value = value
16
+ end
17
+
18
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil)
19
+ output = ''.dup
20
+
21
+ if evaluate(name: @value, current_binding:)
22
+ @children.each do |child|
23
+ # Antlers nodes respond to "render", whereas HTML is stored as a string and output as is.
24
+ output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:) : child) || ''
25
+ end
26
+ end
27
+
28
+ output
29
+ end
30
+ end
31
+ end
@@ -23,8 +23,8 @@ module Antlers
23
23
  # TODO: Get LowLoad to load constants defined in "<{ MyNode }>" syntax so that we can resolve namespace/params on class load.
24
24
  klass = class_constant(namespace: @namespace&.split('::') || [], name: @name)
25
25
  class_proxy = Lowkey[klass.to_s].first[klass.to_s]
26
- instance = create_instance(class_proxy:, klass:, event:)
27
26
 
27
+ instance = create_instance(class_proxy:, klass:, event:, props:)
28
28
  return instance.render_template(event:, parent_binding:, props:) if klass.template
29
29
 
30
30
  render_args(class_proxy:, instance:, event:, props:)
@@ -32,9 +32,10 @@ module Antlers
32
32
 
33
33
  private
34
34
 
35
- def create_instance(class_proxy:, klass:, event:)
35
+ def create_instance(class_proxy:, klass:, event:, props:)
36
36
  initialize_params = class_proxy.instance_methods[:initialize]&.tagged_params(:keyword)&.map(&:name) || []
37
37
  return klass.new(event:, **props) if initialize_params.include?(:event) && initialize_params.count > 1
38
+ return klass.new(**props) if initialize_params.count > 1
38
39
  return klass.new(event:) if initialize_params.include?(:event)
39
40
 
40
41
  klass.new
@@ -11,14 +11,19 @@ module Antlers
11
11
 
12
12
  attr_reader :value
13
13
 
14
- def initialize(value:, name: :var)
14
+ def initialize(value:, name: :var, raw: false)
15
15
  super(name:)
16
16
 
17
17
  @value = value
18
+ @raw = raw
18
19
  end
19
20
 
20
21
  def render(current_binding: nil, parent_binding: nil, slot_node: nil)
21
- ERB::Util.html_escape(evaluate(name: @value, current_binding:) || fallback(@value))
22
+ result = evaluate(name: @value, current_binding:) || fallback(@value)
23
+
24
+ return result if @raw
25
+
26
+ ERB::Util.html_escape(result)
22
27
  end
23
28
  end
24
29
  end
data/lib/parser.rb CHANGED
@@ -21,6 +21,8 @@ module Antlers
21
21
  node.children << segment
22
22
  elsif segment[:var]
23
23
  node.children << @factory.var_node(segment:)
24
+ elsif segment[:raw_var]
25
+ node.children << @factory.raw_var_node(segment:)
24
26
  elsif segment[:prop]
25
27
  node.children << @factory.prop_node(segment:)
26
28
  elsif segment[:slot]
@@ -29,6 +31,10 @@ module Antlers
29
31
  slot_node = @factory.slot_node(segment:)
30
32
  node.children << slot_node
31
33
  sub_branch(node: slot_node, sequence:, end_key: :slot_end, end_name: slot_node.name)
34
+ elsif segment[:if_def]
35
+ if_node = @factory.if_node(segment:)
36
+ node.children << if_node
37
+ sub_branch(node: if_node, sequence:, end_key: :if_end, end_name: 'level_1')
32
38
  elsif segment[:for_def]
33
39
  for_node = @factory.for_node(segment:)
34
40
  node.children << for_node
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antlers
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.1'
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.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -64,12 +64,20 @@ files:
64
64
  - lib/interfaces/antler_node.rb
65
65
  - lib/interfaces/branch_node.rb
66
66
  - lib/interfaces/leaf_node.rb
67
+ - lib/interfaces/lexeme.rb
68
+ - lib/lexemes/for_lexeme.rb
69
+ - lib/lexemes/form_lexeme.rb
70
+ - lib/lexemes/if_lexeme.rb
71
+ - lib/lexemes/prop_lexeme.rb
72
+ - lib/lexemes/slot_lexeme.rb
73
+ - lib/lexemes/yield_lexeme.rb
67
74
  - lib/lexer.rb
68
75
  - lib/modules/namespace.rb
69
76
  - lib/modules/props.rb
70
77
  - lib/modules/variables.rb
71
78
  - lib/nodes/for_node.rb
72
79
  - lib/nodes/form_node.rb
80
+ - lib/nodes/if_node.rb
73
81
  - lib/nodes/prop_node.rb
74
82
  - lib/nodes/root_node.rb
75
83
  - lib/nodes/slot_node.rb