antlers 0.8.0 → 0.8.2

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: b0d88855d8351d4ce9b4ebf2bfeaf0fc05faef8a33a882ffd206fa1c8241ec9e
4
- data.tar.gz: e72f72db347e9f0f442e0a15cb4dce3f2c7405bf1bd7da6b33ab919a7003785b
3
+ metadata.gz: e0dbec268b4fe0b7966b5f7161c61b16c155695ae10ab9c16d66ea67462348f4
4
+ data.tar.gz: a5ef4b1479077100df98ebf33a16379cf934cecbb980184008b5ecb86c3bcbb0
5
5
  SHA512:
6
- metadata.gz: 6f1e6d6f8f512d327ee25068a3286d8f1e557407bf99f4ec274cd8c8e7336ef9c2d9db9980e66d13ce58b8055d07f6908108afcb8cd91d2da65224ac567946d4
7
- data.tar.gz: 27520e044188ae13cde780d7438286e510dae3874cd2218debf2901a1b3c7e9909847c44ae38ce4353e8b0482afa2516fa47857836070a58e8683934865a58c7
6
+ metadata.gz: e8e4537ad73ba6356a5ce6a4632da46113374c21e6449ba9f744f9166e47ec93fcb619cab17d536d0b6413145c11e91f726b0a48e6b628af35c3662ec55e2970
7
+ data.tar.gz: 34b6b683e8ca4d3b736efa8684d049064032abf7ca081d2791370e0135b8ff7adc02e67dcf22a3c32dbe616b84302882b5346cc28c0915145e5278252cab5048
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plugs'
4
+
5
+ module Antlers
6
+ class Elements
7
+ include Plugs
8
+
9
+ plug(:root) do
10
+ plug(:node) do
11
+ lazy do
12
+ require_relative '../nodes/root_node'
13
+ RootNode
14
+ end
15
+ end
16
+ end
17
+
18
+ plug(:html) do
19
+ plug(:node) do
20
+ lazy do
21
+ require_relative '../nodes/html_node'
22
+ HTMLNode
23
+ end
24
+ end
25
+ end
26
+
27
+ plug(:form) do
28
+ plug(:lexeme) do
29
+ lazy do
30
+ require_relative '../lexemes/form_lexeme'
31
+ FormLexeme
32
+ end
33
+ end
34
+
35
+ plug(:node) do
36
+ lazy do
37
+ require_relative '../nodes/form_node'
38
+ FormNode
39
+ end
40
+ end
41
+ end
42
+
43
+ plug(:for) do
44
+ plug(:lexeme) do
45
+ lazy do
46
+ require_relative '../lexemes/for_lexeme'
47
+ ForLexeme
48
+ end
49
+ end
50
+
51
+ plug(:node) do
52
+ lazy do
53
+ require_relative '../nodes/for_node'
54
+ ForNode
55
+ end
56
+ end
57
+ end
58
+
59
+ plug(:if) do
60
+ plug(:lexeme) do
61
+ lazy do
62
+ require_relative '../lexemes/if_lexeme'
63
+ IfLexeme
64
+ end
65
+ end
66
+
67
+ plug(:node) do
68
+ lazy do
69
+ require_relative '../nodes/if_node'
70
+ IfNode
71
+ end
72
+ end
73
+ end
74
+
75
+ plug(:prop) do
76
+ plug(:lexeme) do
77
+ lazy do
78
+ require_relative '../lexemes/prop_lexeme'
79
+ PropLexeme
80
+ end
81
+ end
82
+
83
+ plug(:node) do
84
+ lazy do
85
+ require_relative '../nodes/prop_node'
86
+ PropNode
87
+ end
88
+ end
89
+ end
90
+
91
+ plug(:slot) do
92
+ plug(:lexeme) do
93
+ lazy do
94
+ require_relative '../lexemes/slot_lexeme'
95
+ SlotLexeme
96
+ end
97
+ end
98
+
99
+ plug(:node) do
100
+ lazy do
101
+ require_relative '../nodes/slot_node'
102
+ SlotNode
103
+ end
104
+ end
105
+ end
106
+
107
+ plug(:yield) do
108
+ plug(:lexeme) do
109
+ lazy do
110
+ require_relative '../lexemes/yield_lexeme'
111
+ YieldLexeme
112
+ end
113
+ end
114
+
115
+ plug(:node) do
116
+ lazy do
117
+ require_relative '../nodes/yield_node'
118
+ YieldNode
119
+ end
120
+ end
121
+ end
122
+
123
+ plug(:var) do
124
+ plug(:node) do
125
+ lazy do
126
+ require_relative '../nodes/var_node'
127
+ VarNode
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../support/queries'
4
+
5
+ module Antlers
6
+ extend Queries
7
+
8
+ class LexerError < StandardError; end
9
+
10
+ class Lexer
11
+ def initialize(lexeme_types:)
12
+ @delimiters = ['<{', '}>', '{', '}']
13
+ @lexeme_types = lexeme_types
14
+ @keywords = @lexeme_types.flat_map { |lexeme| lexeme.const_get(:KEYWORDS) }
15
+ @cursor = 0
16
+ end
17
+
18
+ def parse(template:)
19
+ @cursor = 0
20
+ sequence = []
21
+
22
+ # Split on delimiters and retain capture groups.
23
+ segments = template.split(/(#{Regexp.union(@delimiters)})/).map(&:strip)
24
+
25
+ until segments[@cursor].nil?
26
+ if (antlers_segment = antlers_segment(segments:))
27
+ sequence << antlers_lexeme(antlers_segment:, segments:)
28
+ # Skipping: ['{', 'expression', '}']
29
+ # Skipping: ['<{', 'name + props + keywords', '}>']
30
+ @cursor += 3
31
+ else
32
+ segment = segments[@cursor]
33
+ sequence << segment unless segment.empty?
34
+ @cursor += 1
35
+ end
36
+ end
37
+
38
+ sequence
39
+ end
40
+
41
+ private
42
+
43
+ def antlers_segment(segments:)
44
+ next_segment = segments[@cursor + 1]
45
+ return nil unless next_segment && (segments[@cursor] == '<{' || brackets?(segments:))
46
+
47
+ next_segment
48
+ end
49
+
50
+ def antlers_lexeme(antlers_segment:, segments:)
51
+ return var(antlers_segment:) if brackets?(segments:)
52
+
53
+ name, props, keywords = parse_segment(antlers_segment:)
54
+
55
+ lexeme = @lexeme_types.find { it.match?(name:, keywords:) }
56
+ return lexeme.lexeme(name:, props:, keywords:) if lexeme
57
+ return var(antlers_segment:, raw: true) if deerheads?(segments:)
58
+
59
+ raise LexerError, "Unrecognised syntax: '#{antlers_segment}'"
60
+ end
61
+
62
+ def parse_segment(antlers_segment:)
63
+ name_and_props, *keywords = antlers_segment.split(/(#{Regexp.union(@keywords)})/)
64
+ name, *props = name_and_props.split(' ')
65
+ [name, props, parse_keywords(keywords:)]
66
+ end
67
+
68
+ def parse_keywords(keywords:)
69
+ key_values = {}
70
+
71
+ while (keyword = keywords.shift)
72
+ keyword.strip!
73
+ value = keyword.end_with?(':') && value?(keywords.first.strip) ? keywords.shift.strip : nil
74
+ key_values[keyword] = value
75
+ end
76
+
77
+ key_values
78
+ end
79
+
80
+ def value?(string)
81
+ !(string.start_with?(':') || string.end_with?(':'))
82
+ end
83
+
84
+ # TODO: Refactor every constant, match and result method into its own class. Loop through every class and return the first match.
85
+
86
+ def brackets?(segments:)
87
+ first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
88
+ first == '{' && last == '}'
89
+ end
90
+
91
+ def deerheads?(segments:)
92
+ first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
93
+ first == '<{' && last == '}>'
94
+ end
95
+
96
+ def var(antlers_segment:, raw: false)
97
+ # String is already interpolated or not depending on user input on the template layer, now we store it without those template quotes.
98
+ antlers_segment = antlers_segment[1..-2] if Queries.user_defined_string?(antlers_segment)
99
+
100
+ return { raw_var: antlers_segment } if raw
101
+
102
+ { var: antlers_segment }
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antlers
4
+ class Parser
5
+ class ParserError < StandardError; end
6
+
7
+ def initialize(namespace: nil, node_types:)
8
+ @namespace = namespace
9
+ @node_types = node_types
10
+ end
11
+
12
+ def parse(sequence:, id: :root_node)
13
+ branch(node: RootNode.new(name: id), sequence:)
14
+ end
15
+
16
+ def branch(node:, sequence:)
17
+ until sequence.empty?
18
+ segment = sequence.shift
19
+
20
+ child_class = @node_types.find { |n| n.match?(segment:) }
21
+ raise(ParserError, "No node matches #{segment}") unless child_class
22
+
23
+ child = child_class.build(segment:, namespace: @namespace)
24
+ node.children << child
25
+
26
+ if (end_key = child_class::END_KEY)
27
+ sub_branch(node: child, sequence:, end_key:, end_name: child.end_name)
28
+ end
29
+ end
30
+
31
+ node
32
+ end
33
+
34
+ def sub_branch(node:, sequence:, end_key:, end_name: nil)
35
+ sub_sequence = []
36
+ sub_sequence << sequence.shift until sequence.first.is_a?(Hash) && sequence.first[end_key] == end_name
37
+ # Remove the end tag we just stopped before.
38
+ sequence.shift
39
+
40
+ branch(node:, sequence: sub_sequence)
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antlers
4
- VERSION = '0.8.0'
4
+ VERSION = '0.8.2'
5
5
  end
data/lib/antlers.rb CHANGED
@@ -1,15 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lexer'
4
- require_relative 'parser'
3
+ require_relative 'antlers/elements'
4
+ require_relative 'antlers/lexer'
5
+ require_relative 'antlers/parser'
6
+
7
+ # These requires allow downstream libraries to build their own antlers nodes.
8
+ require_relative 'interfaces/branch_node'
9
+ require_relative 'interfaces/leaf_node'
5
10
 
6
11
  module Antlers
12
+ DEFAULT_ELEMENTS = [:root, :html, :form, :for, :if, :prop, :slot, :yield, :var]
13
+
7
14
  class << self
8
- def ast(template:, namespace: nil)
15
+ def ast(template:, namespace: nil, elements: Elements[*DEFAULT_ELEMENTS])
9
16
  return template unless template.include?('<{') || template.include?('{')
10
17
 
11
- sequence = Lexer.new.parse(template)
12
- Parser.new(namespace:).parse(sequence:)
18
+ sequence = Lexer.new(lexeme_types: elements[:lexeme].to_a).parse(template:)
19
+ Parser.new(namespace:, node_types: elements[:node].to_a).parse(sequence:)
13
20
  end
14
21
 
15
22
  def render(ast:, current_binding:, parent_binding: nil, slot_node: nil)
@@ -4,6 +4,9 @@ module Antlers
4
4
  class AntlerNode
5
5
  attr_reader :name
6
6
 
7
+ DEF_KEY = nil
8
+ END_KEY = nil
9
+
7
10
  def initialize(name:)
8
11
  @name = name
9
12
  end
@@ -12,7 +15,21 @@ module Antlers
12
15
  raise NotImplementedError
13
16
  end
14
17
 
15
- # Consider instance a value object on comparison.
18
+ def end_name
19
+ 'level_1'
20
+ end
21
+
22
+ class << self
23
+ def match?(segment:)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def build(segment:, namespace:)
28
+ raise NotImplementedError
29
+ end
30
+ end
31
+
32
+ # Compare instance as a value object.
16
33
  def ==(other) = other.class == self.class
17
34
  def eql?(other) = self == other
18
35
  def hash = [self.class].hash
@@ -16,8 +16,7 @@ module Antlers
16
16
  output = ''
17
17
 
18
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:) : child) || ''
19
+ output += child.render(current_binding:, parent_binding:, slot_node:) || ''
21
20
  end
22
21
 
23
22
  output
@@ -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
@@ -6,7 +6,7 @@ module Antlers
6
6
  module Variables
7
7
  extend Queries
8
8
 
9
- # Evaluation limited to the following for security and to prevent a templating language becoming a programming language.
9
+ # Evaluation limited to the following for security and to stop a templating language becoming a programming language.
10
10
  # 1. An instance variable
11
11
  # 2. A method call/local variable
12
12
  # 3. A method chain
@@ -9,6 +9,9 @@ module Antlers
9
9
  include Props
10
10
  include Variables
11
11
 
12
+ DEF_KEY = :for_def
13
+ END_KEY = :for_end
14
+
12
15
  def initialize(name:, items:, value:, key: nil, props: [], children: [])
13
16
  super(name:, props:, children:)
14
17
 
@@ -28,12 +31,22 @@ module Antlers
28
31
  current_binding.local_variable_set(@key, key) if @key
29
32
 
30
33
  @children.each do |child|
31
- # Antlers nodes respond to "render", whereas HTML is stored as a string and output as is.
32
- output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:) : child) || ''
34
+ output += child.render(current_binding:, parent_binding:, slot_node:) || ''
33
35
  end
34
36
  end
35
37
 
36
38
  output
37
39
  end
40
+
41
+ class << self
42
+ def match?(segment:)
43
+ segment[DEF_KEY]
44
+ end
45
+
46
+ def build(segment:, **)
47
+ value, key, items = segment.values_at(DEF_KEY, :key, :in)
48
+ new(name: value, key:, value:, items:)
49
+ end
50
+ end
38
51
  end
39
52
  end
@@ -9,6 +9,9 @@ module Antlers
9
9
  include Props
10
10
  include Variables
11
11
 
12
+ DEF_KEY = :form_def
13
+ END_KEY = :form_end
14
+
12
15
  def initialize(name:, action: nil, method: 'POST', props: [], children: [])
13
16
  super(name:, props:, children:)
14
17
 
@@ -20,12 +23,24 @@ module Antlers
20
23
  output = "<form action='#{@action}' method='#{@method}'>"
21
24
 
22
25
  @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) || ''
26
+ output += child.render(current_binding:, parent_binding:, slot_node:) || ''
25
27
  end
26
28
 
27
29
  output += '</form>'
28
30
  output
29
31
  end
32
+
33
+ class << self
34
+ def match?(segment:)
35
+ segment[DEF_KEY]
36
+ end
37
+
38
+ def build(segment:, **)
39
+ action, method = segment.values_at(DEF_KEY, :method)
40
+ return new(name: action, action:, method:) if method
41
+
42
+ new(name: action, action:)
43
+ end
44
+ end
30
45
  end
31
46
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/leaf_node'
4
+
5
+ module Antlers
6
+ class HTMLNode < LeafNode
7
+ def initialize(html:)
8
+ super(name: nil)
9
+
10
+ @html = html
11
+ end
12
+
13
+ def render(**)
14
+ @html
15
+ end
16
+
17
+ class << self
18
+ def match?(segment:)
19
+ segment.is_a?(String)
20
+ end
21
+
22
+ def build(segment:, **)
23
+ new(html: segment)
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/nodes/if_node.rb CHANGED
@@ -9,6 +9,9 @@ module Antlers
9
9
  include Props
10
10
  include Variables
11
11
 
12
+ DEF_KEY = :if_def
13
+ END_KEY = :if_end
14
+
12
15
  def initialize(name:, value:, props: [], children: [])
13
16
  super(name:, props:, children:)
14
17
 
@@ -20,12 +23,21 @@ module Antlers
20
23
 
21
24
  if evaluate(name: @value, current_binding:)
22
25
  @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) || ''
26
+ output += child.render(current_binding:, parent_binding:, slot_node:) || ''
25
27
  end
26
28
  end
27
29
 
28
30
  output
29
31
  end
32
+
33
+ class << self
34
+ def match?(segment:)
35
+ segment[DEF_KEY]
36
+ end
37
+
38
+ def build(segment:, **)
39
+ new(name: segment[DEF_KEY], value: segment[DEF_KEY])
40
+ end
41
+ end
30
42
  end
31
43
  end
@@ -23,18 +23,29 @@ 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:)
31
31
  end
32
32
 
33
+ class << self
34
+ def match?(segment:)
35
+ segment[:prop]
36
+ end
37
+
38
+ def build(segment:, namespace:)
39
+ new(name: segment[:prop], props: segment[:props], namespace:)
40
+ end
41
+ end
42
+
33
43
  private
34
44
 
35
- def create_instance(class_proxy:, klass:, event:)
45
+ def create_instance(class_proxy:, klass:, event:, props:)
36
46
  initialize_params = class_proxy.instance_methods[:initialize]&.tagged_params(:keyword)&.map(&:name) || []
37
47
  return klass.new(event:, **props) if initialize_params.include?(:event) && initialize_params.count > 1
48
+ return klass.new(**props) if initialize_params.count > 1
38
49
  return klass.new(event:) if initialize_params.include?(:event)
39
50
 
40
51
  klass.new
@@ -7,5 +7,15 @@ module Antlers
7
7
  def initialize(name: :root_node, children: [])
8
8
  super(name:, children:)
9
9
  end
10
+
11
+ class << self
12
+ def match?(**)
13
+ false
14
+ end
15
+
16
+ def build(**)
17
+ new()
18
+ end
19
+ end
10
20
  end
11
21
  end
@@ -7,7 +7,10 @@ require_relative '../modules/props'
7
7
  module Antlers
8
8
  class SlotNode < BranchNode
9
9
  include Namespace
10
- include Props # Immediate parent ancestor which props are passed to.
10
+ include Props # The immediate ancestor which props are passed to.
11
+
12
+ DEF_KEY = :slot_def
13
+ END_KEY = :slot_end
11
14
 
12
15
  attr_accessor :children
13
16
 
@@ -30,5 +33,19 @@ module Antlers
30
33
 
31
34
  props.empty? ? instance.render(event:) : instance.render(event:, **props)
32
35
  end
36
+
37
+ def end_name
38
+ @name
39
+ end
40
+
41
+ class << self
42
+ def match?(segment:)
43
+ segment[DEF_KEY]
44
+ end
45
+
46
+ def build(segment:, namespace:)
47
+ new(name: segment[DEF_KEY], props: segment[:props], namespace:)
48
+ end
49
+ end
33
50
  end
34
51
  end
@@ -25,5 +25,15 @@ module Antlers
25
25
 
26
26
  ERB::Util.html_escape(result)
27
27
  end
28
+
29
+ class << self
30
+ def match?(segment:)
31
+ segment[:var] || segment[:raw_var]
32
+ end
33
+
34
+ def build(segment:, **)
35
+ new(value: segment[:var] || segment[:raw_var], raw: !!segment[:raw_var])
36
+ end
37
+ end
28
38
  end
29
39
  end
@@ -8,20 +8,25 @@ module Antlers
8
8
  super(name:)
9
9
  end
10
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)
11
+ # Render the children in the binding of the parent.
12
+ def render(current_binding: nil, parent_binding:, slot_node:)
13
13
  output = ''
14
14
 
15
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
- if child.respond_to?(:render) # rubocop:disable Style/ConditionalAssignment
18
- output += child.render(current_binding: parent_binding, parent_binding: nil, slot_node: nil)
19
- else
20
- output += child || ''
21
- end
16
+ output += child.render(current_binding: parent_binding, parent_binding:, slot_node:) || ''
22
17
  end
23
18
 
24
19
  output
25
20
  end
21
+
22
+ class << self
23
+ def match?(segment:)
24
+ segment[:slot]
25
+ end
26
+
27
+ def build(segment:, **)
28
+ new(name: segment[:slot])
29
+ end
30
+ end
26
31
  end
27
32
  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.8.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: plugs
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  description: A templating language embedded within HTML that is itself embedded within
55
69
  Ruby/RBX
56
70
  email:
@@ -60,25 +74,33 @@ extensions: []
60
74
  extra_rdoc_files: []
61
75
  files:
62
76
  - lib/antlers.rb
63
- - lib/factories/node_factory.rb
77
+ - lib/antlers/elements.rb
78
+ - lib/antlers/lexer.rb
79
+ - lib/antlers/parser.rb
80
+ - lib/antlers/version.rb
64
81
  - lib/interfaces/antler_node.rb
65
82
  - lib/interfaces/branch_node.rb
66
83
  - lib/interfaces/leaf_node.rb
67
- - lib/lexer.rb
84
+ - lib/interfaces/lexeme.rb
85
+ - lib/lexemes/for_lexeme.rb
86
+ - lib/lexemes/form_lexeme.rb
87
+ - lib/lexemes/if_lexeme.rb
88
+ - lib/lexemes/prop_lexeme.rb
89
+ - lib/lexemes/slot_lexeme.rb
90
+ - lib/lexemes/yield_lexeme.rb
68
91
  - lib/modules/namespace.rb
69
92
  - lib/modules/props.rb
70
93
  - lib/modules/variables.rb
71
94
  - lib/nodes/for_node.rb
72
95
  - lib/nodes/form_node.rb
96
+ - lib/nodes/html_node.rb
73
97
  - lib/nodes/if_node.rb
74
98
  - lib/nodes/prop_node.rb
75
99
  - lib/nodes/root_node.rb
76
100
  - lib/nodes/slot_node.rb
77
101
  - lib/nodes/var_node.rb
78
102
  - lib/nodes/yield_node.rb
79
- - lib/parser.rb
80
103
  - lib/support/queries.rb
81
- - lib/version.rb
82
104
  homepage: https://github.com/raindeer-rb/antlers
83
105
  licenses: []
84
106
  metadata:
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../nodes/if_node'
4
- require_relative '../nodes/for_node'
5
- require_relative '../nodes/form_node'
6
- require_relative '../nodes/prop_node'
7
- require_relative '../nodes/slot_node'
8
- require_relative '../nodes/var_node'
9
- require_relative '../nodes/yield_node'
10
-
11
- module Antlers
12
- class NodeFactory
13
- def initialize(namespace:)
14
- @namespace = namespace
15
- end
16
-
17
- def if_node(segment:)
18
- IfNode.new(name: segment[:if_def], value: segment[:if_def])
19
- end
20
-
21
- def for_node(segment:)
22
- value, key, items = segment.values_at(:for_def, :key, :in)
23
- ForNode.new(name: value, key:, value:, items:)
24
- end
25
-
26
- def form_node(segment:)
27
- action, method = segment.values_at(:form_def, :method)
28
- return FormNode.new(name: action, action:, method:) if method
29
-
30
- FormNode.new(name: action, action:)
31
- end
32
-
33
- def prop_node(segment:)
34
- PropNode.new(name: segment[:prop], props: segment[:props], namespace:)
35
- end
36
-
37
- def slot_node(segment:)
38
- SlotNode.new(name: segment[:slot_def], props: segment[:props], namespace:)
39
- end
40
-
41
- def var_node(segment:)
42
- VarNode.new(value: segment[:var])
43
- end
44
-
45
- def raw_var_node(segment:)
46
- VarNode.new(value: segment[:raw_var], raw: true)
47
- end
48
-
49
- def yield_node(segment:)
50
- YieldNode.new(name: segment[:slot])
51
- end
52
-
53
- private
54
-
55
- attr_reader :namespace
56
- end
57
- end
data/lib/lexer.rb DELETED
@@ -1,204 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'support/queries'
4
-
5
- module Antlers
6
- extend Queries
7
-
8
- class LexerParseError < StandardError; end
9
-
10
- class Lexer
11
- IF_KEYWORDS = ['if:', ':if'].freeze
12
- FOR_KEYWORDS = ['for:', 'in:', ':for'].freeze
13
- FORM_KEYWORDS = ['form:', ':form'].freeze
14
-
15
- def initialize
16
- @delimiters = ['<{', '}>', '{', '}']
17
- @keywords = [*IF_KEYWORDS, *FORM_KEYWORDS, *FOR_KEYWORDS, 'slot:', ':slot']
18
- @cursor = 0
19
- end
20
-
21
- def parse(template)
22
- @cursor = 0
23
- sequence = []
24
-
25
- # Split on delimiters and retain capture groups.
26
- segments = template.split(/(#{Regexp.union(@delimiters)})/).map(&:strip)
27
-
28
- until segments[@cursor].nil?
29
- if (antlers_segment = antlers_segment(segments:))
30
- sequence << antlers_lexeme(antlers_segment:, segments:)
31
- # Skipping: ['{', 'expression', '}']
32
- # Skipping: ['<{', 'name + props + keywords', '}>']
33
- @cursor += 3
34
- else
35
- segment = segments[@cursor]
36
- sequence << segment unless segment.empty?
37
- @cursor += 1
38
- end
39
- end
40
-
41
- sequence
42
- end
43
-
44
- private
45
-
46
- def antlers_segment(segments:)
47
- next_segment = segments[@cursor + 1]
48
- return nil unless next_segment && (segments[@cursor] == '<{' || brackets?(segments:))
49
-
50
- next_segment
51
- end
52
-
53
- def antlers_lexeme(antlers_segment:, segments:)
54
- return var(antlers_segment:) if brackets?(segments:)
55
-
56
- name, props, keywords = parse_segment(antlers_segment:)
57
-
58
- return slot_yield if slot_yield?(keywords:)
59
- return slot(name:, props:) if slot?(name)
60
- return prop(name:, props:) if prop?(name)
61
- return if_block(keywords:) if if_block?(keywords:)
62
- return for_loop(keywords:) if for_loop?(keywords:)
63
- return form(keywords:) if form?(keywords:)
64
- return var(antlers_segment:, raw: true) if deerheads?(segments:)
65
-
66
- raise LexerParseError, "Unrecognised syntax: '#{antlers_segment}'"
67
- end
68
-
69
- def parse_segment(antlers_segment:)
70
- name_and_props, *keywords = antlers_segment.split(/(#{Regexp.union(@keywords)})/)
71
- name, *props = name_and_props.split(' ')
72
- [name, props, parse_keywords(keywords:)]
73
- end
74
-
75
- def parse_keywords(keywords:)
76
- key_values = {}
77
-
78
- while (keyword = keywords.shift)
79
- keyword.strip!
80
- value = keyword.end_with?(':') && value?(keywords.first.strip) ? keywords.shift.strip : nil
81
- key_values[keyword] = value
82
- end
83
-
84
- key_values
85
- end
86
-
87
- def value?(string)
88
- !(string.start_with?(':') || string.end_with?(':'))
89
- end
90
-
91
- # TODO: Refactor every constant, match and result method into its own class. Loop through every class and return the first match.
92
-
93
- def brackets?(segments:)
94
- first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
95
- first == '{' && last == '}'
96
- end
97
-
98
- def deerheads?(segments:)
99
- first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
100
- first == '<{' && last == '}>'
101
- end
102
-
103
- def if_block?(keywords:)
104
- IF_KEYWORDS.include?(keywords.keys.first)
105
- end
106
-
107
- def for_loop?(keywords:)
108
- FOR_KEYWORDS.include?(keywords.keys.first)
109
- end
110
-
111
- def form?(keywords:)
112
- FORM_KEYWORDS.include?(keywords.keys.first)
113
- end
114
-
115
- def slot?(name)
116
- name && (name.start_with?(':') || name.end_with?(':'))
117
- end
118
-
119
- def slot_yield?(keywords:)
120
- keywords.keys.include?(':slot')
121
- end
122
-
123
- def prop?(name)
124
- name && [*'A'..'Z'].include?(name[0])
125
- end
126
-
127
- def var(antlers_segment:, raw: false)
128
- # String is already interpolated or not depending on user input on the template layer, now we store it without those template quotes.
129
- antlers_segment = antlers_segment[1..-2] if Queries.user_defined_string?(antlers_segment)
130
-
131
- return { raw_var: antlers_segment } if raw
132
-
133
- { var: antlers_segment }
134
- end
135
-
136
- def if_block(keywords:)
137
- return { if_def: keywords['if:'] } if keywords.key?('if:')
138
-
139
- { if_end: 'level_1' }
140
- end
141
-
142
- def for_loop(keywords:)
143
- if keywords['for:']
144
- *key, value = keywords['for:'].split(',').map(&:strip)
145
- for_def = { for_def: value, in: keywords['in:'] }
146
- for_def[:key] = key.first unless key.empty?
147
- return for_def
148
- end
149
-
150
- # TODO: Keep track of which for loop we're in to allow nested for loops.
151
- { for_end: 'level_1' }
152
- end
153
-
154
- def form(keywords:)
155
- if keywords.key?('form:')
156
- action = keywords['form:'] ? keywords['form:'][1...-1] : nil
157
- return { form_def: action }
158
- end
159
-
160
- { form_end: 'level_1' }
161
- end
162
-
163
- def slot(name:, props:)
164
- if name.end_with?(':')
165
- slot_def = { slot_def: name.delete_suffix(':') }
166
- slot_def[:props] = props(props) unless props.empty?
167
- return slot_def
168
- end
169
-
170
- { slot_end: name.delete_prefix(':') }
171
- end
172
-
173
- def slot_yield
174
- { slot: :default }
175
- end
176
-
177
- def prop(name:, props:)
178
- prop = { prop: name }
179
- prop[:props] = props(props) unless props.empty?
180
- prop
181
- end
182
-
183
- def props(props)
184
- odd_props = props.join(' ').split(/(=)|\s/)
185
-
186
- return {} unless odd_props.any?
187
-
188
- props = {}
189
- until odd_props.empty?
190
- prop = odd_props.shift
191
- value = nil
192
-
193
- if odd_props.first == '='
194
- odd_props.shift
195
- value = odd_props.shift
196
- end
197
-
198
- props[prop.to_sym] = value
199
- end
200
-
201
- props
202
- end
203
- end
204
- end
data/lib/parser.rb DELETED
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'factories/node_factory'
4
- require_relative 'nodes/root_node'
5
-
6
- module Antlers
7
- class Parser
8
- def initialize(namespace: nil)
9
- @factory = NodeFactory.new(namespace:)
10
- end
11
-
12
- def parse(sequence:, id: :root_node)
13
- branch(node: RootNode.new(name: id), sequence:)
14
- end
15
-
16
- def branch(node:, sequence:) # rubocop:disable Metrics/AbcSize
17
- until sequence.empty?
18
- segment = sequence.shift
19
-
20
- if segment.is_a?(String)
21
- node.children << segment
22
- elsif segment[:var]
23
- node.children << @factory.var_node(segment:)
24
- elsif segment[:raw_var]
25
- node.children << @factory.raw_var_node(segment:)
26
- elsif segment[:prop]
27
- node.children << @factory.prop_node(segment:)
28
- elsif segment[:slot]
29
- node.children << @factory.yield_node(segment:)
30
- elsif segment[:slot_def]
31
- slot_node = @factory.slot_node(segment:)
32
- node.children << slot_node
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')
38
- elsif segment[:for_def]
39
- for_node = @factory.for_node(segment:)
40
- node.children << for_node
41
- sub_branch(node: for_node, sequence:, end_key: :for_end, end_name: 'level_1')
42
- elsif segment[:form_def]
43
- form_node = @factory.form_node(segment:)
44
- node.children << form_node
45
- sub_branch(node: form_node, sequence:, end_key: :form_end, end_name: 'level_1')
46
- end
47
- end
48
-
49
- node
50
- end
51
-
52
- def sub_branch(node:, sequence:, end_key:, end_name: nil)
53
- sub_sequence = []
54
- sub_sequence << sequence.shift until sequence.first.is_a?(Hash) && sequence.first[end_key] == end_name
55
-
56
- branch(node:, sequence: sub_sequence)
57
- end
58
- end
59
- end