orb_template 0.1.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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/LICENSE.txt +21 -0
  5. data/Makefile +45 -0
  6. data/README.md +429 -0
  7. data/Rakefile +15 -0
  8. data/lib/orb/ast/abstract_node.rb +27 -0
  9. data/lib/orb/ast/attribute.rb +51 -0
  10. data/lib/orb/ast/block_node.rb +26 -0
  11. data/lib/orb/ast/control_expression_node.rb +27 -0
  12. data/lib/orb/ast/newline_node.rb +22 -0
  13. data/lib/orb/ast/printing_expression_node.rb +29 -0
  14. data/lib/orb/ast/private_comment_node.rb +22 -0
  15. data/lib/orb/ast/public_comment_node.rb +22 -0
  16. data/lib/orb/ast/root_node.rb +11 -0
  17. data/lib/orb/ast/tag_node.rb +208 -0
  18. data/lib/orb/ast/text_node.rb +22 -0
  19. data/lib/orb/ast.rb +19 -0
  20. data/lib/orb/document.rb +19 -0
  21. data/lib/orb/errors.rb +40 -0
  22. data/lib/orb/parser.rb +182 -0
  23. data/lib/orb/patterns.rb +40 -0
  24. data/lib/orb/rails_derp.rb +138 -0
  25. data/lib/orb/rails_template.rb +101 -0
  26. data/lib/orb/railtie.rb +9 -0
  27. data/lib/orb/render_context.rb +36 -0
  28. data/lib/orb/template.rb +72 -0
  29. data/lib/orb/temple/attributes_compiler.rb +114 -0
  30. data/lib/orb/temple/compiler.rb +204 -0
  31. data/lib/orb/temple/engine.rb +40 -0
  32. data/lib/orb/temple/filters.rb +132 -0
  33. data/lib/orb/temple/generators.rb +108 -0
  34. data/lib/orb/temple/identity.rb +16 -0
  35. data/lib/orb/temple/parser.rb +46 -0
  36. data/lib/orb/temple.rb +16 -0
  37. data/lib/orb/token.rb +47 -0
  38. data/lib/orb/tokenizer.rb +757 -0
  39. data/lib/orb/tokenizer2.rb +591 -0
  40. data/lib/orb/utils/erb.rb +40 -0
  41. data/lib/orb/utils/orb.rb +12 -0
  42. data/lib/orb/version.rb +5 -0
  43. data/lib/orb.rb +50 -0
  44. metadata +89 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ class Attribute
6
+ attr_reader :name, :type
7
+
8
+ def initialize(name, type = :str, value = nil)
9
+ @name = name
10
+ @type = type
11
+ @value = value
12
+ end
13
+
14
+ def value
15
+ if @type == :bool || @type == :boolean
16
+ true
17
+ else
18
+ @value
19
+ end
20
+ end
21
+
22
+ def bool?
23
+ @type == :bool || @type == :boolean
24
+ end
25
+
26
+ def expression?
27
+ @type == :expr || @type == :expression
28
+ end
29
+
30
+ def string?
31
+ @type == :str || @type == :string
32
+ end
33
+
34
+ def splat?
35
+ @type == :splat
36
+ end
37
+
38
+ def static?
39
+ string? || bool?
40
+ end
41
+
42
+ def dynamic?
43
+ expression?
44
+ end
45
+
46
+ def directive?
47
+ @name&.start_with?(":")
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ class BlockNode < AbstractNode
6
+ def initialize(token)
7
+ super
8
+ @name = token.value
9
+ @meta = token.meta
10
+ end
11
+
12
+ def name
13
+ @name.to_sym
14
+ end
15
+
16
+ def expression
17
+ @meta.fetch(:expression, false)
18
+ end
19
+
20
+ # TODO: Support render to text for different block types
21
+ def render(_context)
22
+ raise "BlockNode#render not implemented."
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ # A node representing a non-printing expression
6
+ # Non-printing expressions are used for control flow and variable assignment.
7
+ # Any output from a non-printing expression is captured and discarded.
8
+ class ControlExpressionNode < AbstractNode
9
+ BLOCK_RE = /\A(if|unless)\b|\bdo\s*(\|[^|]*\|)?\s*$/
10
+
11
+ attr_reader :expression
12
+
13
+ def initialize(token)
14
+ super
15
+ @expression = token.value
16
+ end
17
+
18
+ def block?
19
+ @expression =~ BLOCK_RE
20
+ end
21
+
22
+ def end?
23
+ @expression.strip == 'end'
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ class NewlineNode < AbstractNode
6
+ attr_accessor :text
7
+
8
+ def initialize(token)
9
+ super
10
+ @text = token.value
11
+ end
12
+
13
+ def render(_context)
14
+ @text
15
+ end
16
+
17
+ def ==(other)
18
+ super && @text == other.text
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ # A node representing a printing expression
6
+ class PrintingExpressionNode < AbstractNode
7
+ BLOCK_RE = /\A(if|unless)\b|\bdo\s*(\|[^|]*\|)?\s*$/
8
+
9
+ attr_reader :expression
10
+
11
+ def initialize(token)
12
+ super
13
+ @expression = token.value
14
+ end
15
+
16
+ def block?
17
+ @expression =~ BLOCK_RE
18
+ end
19
+
20
+ def end?
21
+ @expression.strip == 'end'
22
+ end
23
+
24
+ def render(_context)
25
+ "NOT IMPLEMENTED"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ class PrivateCommentNode < AbstractNode
6
+ attr_accessor :text
7
+
8
+ def initialize(token)
9
+ super
10
+ @text = token.value
11
+ end
12
+
13
+ def render(_context)
14
+ "{!-- #{@text} --}"
15
+ end
16
+
17
+ def ==(other)
18
+ super && @text == other.text
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ class PublicCommentNode < AbstractNode
6
+ attr_accessor :text
7
+
8
+ def initialize(token)
9
+ super
10
+ @text = token.value
11
+ end
12
+
13
+ def render(_context)
14
+ "<!-- #{@text} -->"
15
+ end
16
+
17
+ def ==(other)
18
+ super && @text == other.text
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ class RootNode < AbstractNode
6
+ def render(context = {})
7
+ @children.map { |child| child.render(context) }.join
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ ##
6
+ # Represents a tag node in the AST.
7
+ # A tag node is used to represent an HTML tag or a component tag.
8
+ # The tag node contains information about the tag name, attributes, and directives.
9
+ #
10
+ # It is created by the parser from a Token object produced by the tokenizer.
11
+ class TagNode < AbstractNode
12
+ attr_reader :tag, :meta
13
+
14
+ SLOT_SEPARATOR = ':'
15
+
16
+ ##
17
+ # Create a new TagNode from the given token
18
+ #
19
+ # @param token [Token] the token to create the node from
20
+ #
21
+ # @return [TagNode] the new node
22
+ def initialize(token)
23
+ super
24
+ @tag = token.value
25
+ @meta = token.meta
26
+
27
+ # Parse attributes from the metadata
28
+ @raw_attributes = @meta.fetch(:attributes, []).map do |attr|
29
+ name, type, value = attr
30
+ Attribute.new(name, type, value)
31
+ end
32
+ end
33
+
34
+ ##
35
+ # Render the node to a string
36
+ # @api private
37
+ def render(_context)
38
+ raise "Not implemented"
39
+ end
40
+
41
+ ##
42
+ # Determine whether the TagNode represents an HTML tag
43
+ #
44
+ # @return [Boolean] true if the tag is an HTML tag, false otherwise
45
+ def html_tag?
46
+ @tag.start_with?(/[a-z]/)
47
+ end
48
+
49
+ ##
50
+ # Determine whether the TagNode represents a self-closing (void) tag
51
+ #
52
+ # @return [Boolean] true if the tag is self-closing, false otherwise
53
+ def self_closing?
54
+ @meta.fetch(:self_closing, false)
55
+ end
56
+
57
+ ##
58
+ # Retrieve the attributes for the tag. Parses the internal representation
59
+ # of attributes in the metadata payload and constructs an array of Attribute objects.
60
+ #
61
+ # Use this method rather than attempting to parse the +meta+ object directly.
62
+ #
63
+ # @return [Array<Attribute>] the attributes for the tag
64
+ def attributes
65
+ @attributes ||= @raw_attributes.reject(&:directive?)
66
+ @attributes
67
+ end
68
+
69
+ ##
70
+ # Retrieve the directives for the tag
71
+ #
72
+ # @return [Array<Array<Attribute>>] the directives for the tag
73
+ def directives
74
+ @directives ||= @raw_attributes
75
+ .select(&:directive?)
76
+ .to_h { |attr| [attr.name[1..], attr.value] }
77
+ .transform_keys(&:to_sym)
78
+ @directives
79
+ end
80
+
81
+ ##
82
+ # Remove a directive from the tag, i.e. when it is consumed by the compiler
83
+ #
84
+ def remove_directive(name)
85
+ @directives.delete(name)
86
+ end
87
+
88
+ ##
89
+ # Clear all directives from the tag
90
+ #
91
+ def clear_directives
92
+ @directives = {}
93
+ end
94
+
95
+ ##
96
+ # Retrieve all the static attributes for the tag.
97
+ # A static attribute is one that has a string or boolean value.
98
+ #
99
+ # @return [Array<Attribute>] the static attributes for the tag
100
+ def static_attributes
101
+ attributes.select(&:static?)
102
+ end
103
+
104
+ ##
105
+ # Retrieve all the dynamic attributes for the tag.
106
+ # A dynamic attribute is one that has an expression value.
107
+ #
108
+ # @return [Array<Attribute>] the dynamic attributes for the tag
109
+ def dynamic_attributes
110
+ attributes.select(&:dynamic?)
111
+ end
112
+
113
+ ##
114
+ # Retrieve all the splat attributes for the tag.
115
+ # A splat attribute is one that has a splat type.
116
+ # The splat attribute is used to pass a hash of attributes to the tag at runtime.
117
+ #
118
+ # @return [Array<Attribute>] the splat attributes for the tag
119
+ def splat_attributes
120
+ attributes.select(&:splat?)
121
+ end
122
+
123
+ ##
124
+ # Determine whether the TagNode represents a component tag
125
+ # A component tag is one that starts with an uppercase letter and
126
+ # may contain a slot call on the component.
127
+ #
128
+ # @return [Boolean] true if the tag is a component tag, false otherwise
129
+ def component_tag?
130
+ @tag.start_with?(/[A-Z]/) && @tag.exclude?(SLOT_SEPARATOR)
131
+ end
132
+
133
+ ##
134
+ # Determine whether the TagNode represents a component slot tag
135
+ # A component slot tag is of the form +Component:slot+ and is used to
136
+ # render a slot within a component.
137
+ #
138
+ # @return [Boolean] true if the tag is a component slot tag, false otherwise
139
+ def component_slot_tag?
140
+ @tag.start_with?(/[A-Z]/) && @tag.include?(SLOT_SEPARATOR)
141
+ end
142
+
143
+ ##
144
+ # Retrieve the component name from the tag
145
+ #
146
+ # @return [String] the component name
147
+ def component
148
+ @tag.split(SLOT_SEPARATOR).first
149
+ end
150
+
151
+ ##
152
+ # Retrieve the slot name from the tag
153
+ #
154
+ # @return [String] the slot name
155
+ def slot
156
+ @tag.split(SLOT_SEPARATOR).last.underscore
157
+ end
158
+
159
+ ##
160
+ # Retrieve the module name from the component name
161
+ # The module name is the first part of the component name
162
+ # separated by a period.
163
+ #
164
+ # For example: +MyApp::UI::Button+ would return +MyApp.UI+
165
+ #
166
+ # @return [String] the module name
167
+ def component_module
168
+ component.rsplit('.').first
169
+ end
170
+
171
+ ##
172
+ # Determine whether the tag can be compiled as static HTML
173
+ #
174
+ # @return [Boolean] true if the tag can be compiled as static HTML, false otherwise
175
+ def static?
176
+ splat_attributes.empty?
177
+ end
178
+
179
+ ##
180
+ # Determine whether the tag needs to be compiled as dynamic HTML
181
+ #
182
+ # @return [Boolean] true if the tag needs to be compiled as dynamic HTML, false otherwise
183
+ def dynamic?
184
+ splat_attributes.any?
185
+ end
186
+
187
+ ##
188
+ # Check whether the node has any directives
189
+ #
190
+ # @return [Boolean] true if the directives are present, false otherwise
191
+ def directives?
192
+ directives.any?
193
+ end
194
+
195
+ def compiler_directives?
196
+ directives.any? { |k, _v| k == :if || k == :for }
197
+ end
198
+
199
+ ##
200
+ # Determine whether the tag content should be escaped or treated as verbatim
201
+ #
202
+ # @return [Boolean] true if the tag content should be escaped, false otherwise
203
+ def verbatim?
204
+ @meta.fetch(:verbatim, false)
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ class TextNode < AbstractNode
6
+ attr_accessor :text
7
+
8
+ def initialize(token)
9
+ super
10
+ @text = token.value
11
+ end
12
+
13
+ def render(_context)
14
+ @text
15
+ end
16
+
17
+ def ==(other)
18
+ super && @text == other.text
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/orb/ast.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ module AST
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :AbstractNode
8
+ autoload :RootNode
9
+ autoload :PublicCommentNode
10
+ autoload :PrivateCommentNode
11
+ autoload :TextNode
12
+ autoload :TagNode
13
+ autoload :Attribute
14
+ autoload :PrintingExpressionNode
15
+ autoload :ControlExpressionNode
16
+ autoload :BlockNode
17
+ autoload :NewlineNode
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ class Document
5
+ attr_reader :root
6
+
7
+ def initialize(tokens)
8
+ parse(tokens)
9
+ end
10
+
11
+ def parse(tokens)
12
+ @root ||= Parser.parse(tokens)
13
+ end
14
+
15
+ def render(context)
16
+ @root.render(context)
17
+ end
18
+ end
19
+ end
data/lib/orb/errors.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ORB
4
+ class Error < StandardError
5
+ attr_reader :line
6
+
7
+ def initialize(message = nil, line = nil)
8
+ super(message)
9
+ @line = line
10
+ end
11
+ end
12
+
13
+ # class SyntaxError < StandardError
14
+ # attr_reader :error, :file, :line, :lineno, :column
15
+
16
+ # def initialize(error, file, line, lineno, column)
17
+ # @error = error
18
+ # @file = file || '(__TEMPLATE__)'
19
+ # @line = line.to_s
20
+ # @lineno = lineno
21
+ # @column = column
22
+ # end
23
+
24
+ # def to_s
25
+ # line = @line.lstrip
26
+ # column = @column + line.size - @line.size
27
+ # message = <<~STR
28
+ # #{error}
29
+ # in #{file}, Line #{lineno}, Column #{@column}
30
+
31
+ # #{line}
32
+ # #{' ' * column}^
33
+ # STR
34
+ # end
35
+ # end
36
+
37
+ class SyntaxError < Error; end
38
+ class ParserError < Error; end
39
+ class CompilerError < Error; end
40
+ end
data/lib/orb/parser.rb ADDED
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ module ORB
6
+ # The `Parser` is responsible for converting a list of tokens produced
7
+ # by the `Lexer` into an Abstract Syntax Tree (AST). Any errors encountered
8
+ # during parsing are stored in `@errors` and can be accessed after parsing.
9
+ class Parser
10
+ attr_reader :tokens, :errors
11
+
12
+ class << self
13
+ def parse(tokens, options = {})
14
+ new(tokens, options).parse
15
+ end
16
+ end
17
+
18
+ # Create a new parser instance, use `Parser.parse` instead.
19
+ def initialize(tokens, options = {})
20
+ @tokens = tokens
21
+ @options = options
22
+ @errors = []
23
+
24
+ @root = ORB::AST::RootNode.new
25
+ @nodes = [@root]
26
+ end
27
+
28
+ # Parse the tokens into a tree of nodes. The `@current` index is used to
29
+ # keep track of the current token being parsed within the stream of tokens.
30
+ def parse
31
+ return @root if @tokens.empty?
32
+
33
+ @current = 0
34
+ next_token while @current < @tokens.length
35
+
36
+ # If there are any nodes left in the stack, they are unmatched tokens
37
+ raise ORB::ParserError, "Unmatched #{@nodes.last.class}" if @nodes.length > 1
38
+
39
+ # Return the root node
40
+ @root
41
+ end
42
+
43
+ alias_method :parse!, :parse
44
+
45
+ private
46
+
47
+ def next_token
48
+ token = @tokens[@current]
49
+
50
+ case token.type
51
+ when :public_comment
52
+ parse_public_comment(token)
53
+ when :private_comment
54
+ parse_private_comment(token)
55
+ when :text
56
+ parse_text(token)
57
+ when :printing_expression
58
+ parse_printing_expression(token)
59
+ when :control_expression
60
+ parse_control_expression(token)
61
+ when :block_open
62
+ parse_block_open(token)
63
+ when :block_close
64
+ parse_block_close(token)
65
+ when :tag_open
66
+ parse_tag(token)
67
+ when :tag_close
68
+ parse_tag_close(token)
69
+ when :newline
70
+ parse_newline(token)
71
+ else
72
+ raise ORB::ParserError, "Unknown token type: #{token.inspect}"
73
+ end
74
+ end
75
+
76
+ def parse_public_comment(token)
77
+ node = ORB::AST::PublicCommentNode.new(token)
78
+ current_node.children << node
79
+
80
+ @current += 1
81
+ end
82
+
83
+ def parse_private_comment(token)
84
+ node = ORB::AST::PrivateCommentNode.new(token)
85
+ current_node.children << node
86
+
87
+ @current += 1
88
+ end
89
+
90
+ def parse_text(token)
91
+ node = ORB::AST::TextNode.new(token)
92
+ current_node.children << node
93
+
94
+ @current += 1
95
+ end
96
+
97
+ def parse_tag(token)
98
+ node = ORB::AST::TagNode.new(token)
99
+
100
+ if node.self_closing?
101
+ current_node.children << node
102
+ else
103
+ @nodes << node
104
+ end
105
+
106
+ @current += 1
107
+ end
108
+
109
+ def parse_tag_close(token)
110
+ node = @nodes.pop
111
+ raise(ORB::ParserError, "Unmatched closing tag '#{token.value}'") unless node.is_a?(ORB::AST::TagNode)
112
+
113
+ current_node.children << node
114
+
115
+ @current += 1
116
+ end
117
+
118
+ def parse_printing_expression(token)
119
+ node = ORB::AST::PrintingExpressionNode.new(token)
120
+
121
+ if node.block?
122
+ @nodes << node
123
+ elsif node.end?
124
+ node = @nodes.pop
125
+ current_node.children << node
126
+ else
127
+ current_node.children << node
128
+ end
129
+
130
+ @current += 1
131
+ end
132
+
133
+ def parse_control_expression(token)
134
+ node = ORB::AST::ControlExpressionNode.new(token)
135
+
136
+ if node.block?
137
+ @nodes << node
138
+ elsif node.end?
139
+ node = @nodes.pop
140
+ current_node.children << node
141
+ else
142
+ current_node.children << node
143
+ end
144
+
145
+ @current += 1
146
+ end
147
+
148
+ def parse_block_open(token)
149
+ node = ORB::AST::BlockNode.new(token)
150
+ @nodes << node
151
+
152
+ @current += 1
153
+ end
154
+
155
+ def parse_block_close(token)
156
+ node = @nodes.pop
157
+ raise(ORB::ParserError, "Unmatched closing block '#{token.value}'") unless node.is_a?(ORB::AST::BlockNode)
158
+
159
+ current_node.children << node
160
+
161
+ @current += 1
162
+ end
163
+
164
+ def parse_newline(token)
165
+ node = ORB::AST::NewlineNode.new(token)
166
+ current_node.children << node
167
+
168
+ @current += 1
169
+ end
170
+
171
+ # Helpers
172
+
173
+ def current_node
174
+ @nodes.last
175
+ end
176
+
177
+ # Helper for raising exceptions during parsing
178
+ def parser_error!(message)
179
+ raise ORB::ParserError.new(message, @tokens[@current].line)
180
+ end
181
+ end
182
+ end