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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/Makefile +45 -0
- data/README.md +429 -0
- data/Rakefile +15 -0
- data/lib/orb/ast/abstract_node.rb +27 -0
- data/lib/orb/ast/attribute.rb +51 -0
- data/lib/orb/ast/block_node.rb +26 -0
- data/lib/orb/ast/control_expression_node.rb +27 -0
- data/lib/orb/ast/newline_node.rb +22 -0
- data/lib/orb/ast/printing_expression_node.rb +29 -0
- data/lib/orb/ast/private_comment_node.rb +22 -0
- data/lib/orb/ast/public_comment_node.rb +22 -0
- data/lib/orb/ast/root_node.rb +11 -0
- data/lib/orb/ast/tag_node.rb +208 -0
- data/lib/orb/ast/text_node.rb +22 -0
- data/lib/orb/ast.rb +19 -0
- data/lib/orb/document.rb +19 -0
- data/lib/orb/errors.rb +40 -0
- data/lib/orb/parser.rb +182 -0
- data/lib/orb/patterns.rb +40 -0
- data/lib/orb/rails_derp.rb +138 -0
- data/lib/orb/rails_template.rb +101 -0
- data/lib/orb/railtie.rb +9 -0
- data/lib/orb/render_context.rb +36 -0
- data/lib/orb/template.rb +72 -0
- data/lib/orb/temple/attributes_compiler.rb +114 -0
- data/lib/orb/temple/compiler.rb +204 -0
- data/lib/orb/temple/engine.rb +40 -0
- data/lib/orb/temple/filters.rb +132 -0
- data/lib/orb/temple/generators.rb +108 -0
- data/lib/orb/temple/identity.rb +16 -0
- data/lib/orb/temple/parser.rb +46 -0
- data/lib/orb/temple.rb +16 -0
- data/lib/orb/token.rb +47 -0
- data/lib/orb/tokenizer.rb +757 -0
- data/lib/orb/tokenizer2.rb +591 -0
- data/lib/orb/utils/erb.rb +40 -0
- data/lib/orb/utils/orb.rb +12 -0
- data/lib/orb/version.rb +5 -0
- data/lib/orb.rb +50 -0
- metadata +89 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ORB
|
|
4
|
+
module Temple
|
|
5
|
+
# The Compiler is used in the ORB::Engine to compile an input document string
|
|
6
|
+
# into a Temple expression that gets passed on to the next step in the pipeline.
|
|
7
|
+
class Compiler
|
|
8
|
+
# The Compiler is initialized by the Engine pipeline with the options passed to the Engine
|
|
9
|
+
def initialize(options = {})
|
|
10
|
+
@options = options
|
|
11
|
+
@identity = Identity.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Then the pipeline executes the #call method with an ORB::AST::*Node input
|
|
15
|
+
def call(ast)
|
|
16
|
+
return runtime_error(ast) if ast.is_a?(ORB::Error)
|
|
17
|
+
|
|
18
|
+
transform(ast)
|
|
19
|
+
rescue CompilerError => e
|
|
20
|
+
raise e
|
|
21
|
+
rescue ORB::Error => e
|
|
22
|
+
runtime_error(e)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Entry point for the compiler, dispatches to the appropriate method based on the node type
|
|
28
|
+
# The `context` argument is used to pass information down the tree of nodes
|
|
29
|
+
# The compile method is usually called on a root node, which then calls `compile` on its children
|
|
30
|
+
# even though it can be called on any node in the AST to compile the subtree under that node.
|
|
31
|
+
#
|
|
32
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
33
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
34
|
+
def transform(node, context = {})
|
|
35
|
+
if node.is_a?(ORB::AST::RootNode)
|
|
36
|
+
transform_children(node, context)
|
|
37
|
+
elsif node.is_a?(ORB::AST::TextNode)
|
|
38
|
+
transform_text_node(node, context)
|
|
39
|
+
elsif node.is_a?(ORB::AST::PrintingExpressionNode)
|
|
40
|
+
transform_printing_expression_node(node, context)
|
|
41
|
+
elsif node.is_a?(ORB::AST::ControlExpressionNode)
|
|
42
|
+
transform_control_expression_node(node, context)
|
|
43
|
+
elsif node.is_a?(ORB::AST::TagNode) && node.compiler_directives?
|
|
44
|
+
transform_directives_for_tag_node(node, context)
|
|
45
|
+
elsif node.is_a?(ORB::AST::TagNode) && node.dynamic?
|
|
46
|
+
transform_dynamic_tag_node(node, context)
|
|
47
|
+
elsif node.is_a?(ORB::AST::TagNode) && node.html_tag?
|
|
48
|
+
transform_html_tag_node(node, context)
|
|
49
|
+
elsif node.is_a?(ORB::AST::TagNode) && node.component_tag?
|
|
50
|
+
transform_component_tag_node(node, context)
|
|
51
|
+
elsif node.is_a?(ORB::AST::TagNode) && node.component_slot_tag?
|
|
52
|
+
transform_component_slot_tag_node(node, context)
|
|
53
|
+
elsif node.is_a?(ORB::AST::BlockNode)
|
|
54
|
+
transform_block_node(node, context)
|
|
55
|
+
elsif node.is_a?(ORB::AST::PublicCommentNode)
|
|
56
|
+
transform_public_comment_node(node, context)
|
|
57
|
+
elsif node.is_a?(ORB::AST::PrivateCommentNode)
|
|
58
|
+
transform_private_comment_node(node, context)
|
|
59
|
+
elsif node.is_a?(ORB::AST::NewlineNode)
|
|
60
|
+
transform_newline_node(node, context)
|
|
61
|
+
elsif node.is_a?(ORB::Error)
|
|
62
|
+
runtime_error(node)
|
|
63
|
+
else
|
|
64
|
+
raise ORB::CompilerError, "Unknown node type: #{node.class} for #{node.inspect}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
68
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
69
|
+
|
|
70
|
+
# Compile the children of a node and collect the result into a Temple expression
|
|
71
|
+
def transform_children(node, context)
|
|
72
|
+
[:multi, *node.children.map { |child| transform(child, context) }]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Compile a TextNode into a Temple expression
|
|
76
|
+
def transform_text_node(node, _context)
|
|
77
|
+
[:static, node.text]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Compile an PExpressionNode into a Temple expression
|
|
81
|
+
def transform_printing_expression_node(node, context)
|
|
82
|
+
if node.block?
|
|
83
|
+
tmp = @identity.generate(:variable)
|
|
84
|
+
[:multi,
|
|
85
|
+
# Capture the result of the code in a variable. We can't do
|
|
86
|
+
# `[:dynamic, code]` because it's probably not a complete
|
|
87
|
+
# expression (which is a requirement for Temple).
|
|
88
|
+
[
|
|
89
|
+
:block, "#{tmp} = #{node.expression}",
|
|
90
|
+
|
|
91
|
+
# Capture the content of a block in a separate buffer. This means
|
|
92
|
+
# that `yield` will not output the content to the current buffer,
|
|
93
|
+
# but rather return the output.
|
|
94
|
+
#
|
|
95
|
+
# The capturing can be disabled with the option :disable_capture.
|
|
96
|
+
# Output code in the block writes directly to the output buffer then.
|
|
97
|
+
# Rails handles this by replacing the output buffer for helpers.
|
|
98
|
+
if @options.fetch(:disable_capture, false)
|
|
99
|
+
transform_children(node, context)
|
|
100
|
+
else
|
|
101
|
+
[:capture, @identity.generate(:variable), transform_children(node, context)]
|
|
102
|
+
end
|
|
103
|
+
],
|
|
104
|
+
# Output the content.
|
|
105
|
+
[:escape, true, [:dynamic, tmp]]]
|
|
106
|
+
elsif node.children.any?
|
|
107
|
+
[:multi, [:escape, true, [:dynamic, node.expression]], transform_children(node, context)]
|
|
108
|
+
else
|
|
109
|
+
[:escape, true, [:dynamic, node.expression]]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Compile an NPExpressionNode into a Temple expression
|
|
114
|
+
def transform_control_expression_node(node, context)
|
|
115
|
+
if node.block?
|
|
116
|
+
tmp = @identity.generate(:variable)
|
|
117
|
+
[:multi,
|
|
118
|
+
[:block, "#{tmp} = #{node.expression}", transform_children(node, context)]]
|
|
119
|
+
elsif node.children.any?
|
|
120
|
+
[:multi, [:code, node.expression], transform_children(node, context)]
|
|
121
|
+
else
|
|
122
|
+
[:code, node.expression]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Compile an HTML TagNode into a Temple expression
|
|
127
|
+
def transform_html_tag_node(node, context)
|
|
128
|
+
[:orb, :tag, node.tag, node.attributes, transform_children(node, context)]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Compile a component TagNode into a Temple expression
|
|
132
|
+
def transform_component_tag_node(node, context)
|
|
133
|
+
[:orb, :component, node, transform_children(node, context)]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Compile a component slot TagNode into a Temple expression
|
|
137
|
+
def transform_component_slot_tag_node(node, context)
|
|
138
|
+
[:orb, :slot, node, transform_children(node, context)]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Compile a block node into a Temple expression
|
|
142
|
+
def transform_block_node(node, context)
|
|
143
|
+
case node.name
|
|
144
|
+
when :if
|
|
145
|
+
[:orb, :if, node.expression, transform_children(node, context)]
|
|
146
|
+
when :for
|
|
147
|
+
[:orb, :for, node.expression, transform_children(node, context)]
|
|
148
|
+
else
|
|
149
|
+
[:static, 'Unknown block node']
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Compile a comment node into a Temple expression
|
|
154
|
+
def transform_public_comment_node(node, _context)
|
|
155
|
+
[:html, :comment, [:static, node.text]]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Compile a private_comment node into a Temple expression
|
|
159
|
+
def transform_private_comment_node(_node, _context)
|
|
160
|
+
[:static, ""]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Compile a newline node into a Temple expression
|
|
164
|
+
def transform_newline_node(_node, _context)
|
|
165
|
+
[:newline]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Compile a tag node with directives
|
|
169
|
+
def transform_directives_for_tag_node(node, context)
|
|
170
|
+
# First, process any :if directives
|
|
171
|
+
if_directive = node.directives.fetch(:if, false)
|
|
172
|
+
if if_directive
|
|
173
|
+
node.remove_directive(:if)
|
|
174
|
+
return [:if,
|
|
175
|
+
if_directive,
|
|
176
|
+
transform(node, context)]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Second, process any :for directives
|
|
180
|
+
for_directive = node.directives.fetch(:for, false)
|
|
181
|
+
if for_directive
|
|
182
|
+
node.remove_directive(:for)
|
|
183
|
+
return [:orb, :for, for_directive, transform(node, context)]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Last, render as a dynamic node expression
|
|
187
|
+
transform(node, context)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Compile a dynamic tag node
|
|
191
|
+
def transform_dynamic_tag_node(node, context)
|
|
192
|
+
[:orb, :dynamic, node, transform_children(node, context)]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Helper or raising exceptions during compilation
|
|
196
|
+
def runtime_error(error)
|
|
197
|
+
[:multi].tap do |temple|
|
|
198
|
+
(error.line - 1).times { temple << [:newline] } if error.line
|
|
199
|
+
temple << [:code, %[raise ORB::Error.new(%q[#{error.message}], #{error.line.inspect})]]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'temple'
|
|
4
|
+
|
|
5
|
+
module ORB
|
|
6
|
+
module Temple
|
|
7
|
+
class Engine < ::Temple::Engine
|
|
8
|
+
# This overwrites some Temple default options or sets default options for ORB specific filters.
|
|
9
|
+
# It is recommended to set the default settings only once in the code and avoid duplication. Only use
|
|
10
|
+
# `define_options` when you have to override some default settings.
|
|
11
|
+
define_options generator: ::Temple::Generators::StringBuffer,
|
|
12
|
+
buffer_class: 'ActionView::OutputBuffer',
|
|
13
|
+
format: :xhtml,
|
|
14
|
+
default_tag: 'div',
|
|
15
|
+
pretty: false,
|
|
16
|
+
attr_quote: '"',
|
|
17
|
+
sort_attrs: true,
|
|
18
|
+
merge_attrs: { 'class' => ' ' },
|
|
19
|
+
streaming: true,
|
|
20
|
+
use_html_safe: true,
|
|
21
|
+
disable_capture: false
|
|
22
|
+
filter :Encoding
|
|
23
|
+
filter :RemoveBOM
|
|
24
|
+
use ORB::Temple::Parser
|
|
25
|
+
use ORB::Temple::Compiler
|
|
26
|
+
use ORB::Temple::Filters
|
|
27
|
+
html :AttributeSorter
|
|
28
|
+
html :AttributeMerger
|
|
29
|
+
use(:AttributeRemover) { ::Temple::HTML::AttributeRemover.new(remove_empty_attrs: options[:merge_attrs].keys) }
|
|
30
|
+
html :Fast
|
|
31
|
+
filter :Ambles
|
|
32
|
+
filter :Escapable
|
|
33
|
+
filter :StaticAnalyzer
|
|
34
|
+
filter :ControlFlow
|
|
35
|
+
filter :MultiFlattener
|
|
36
|
+
filter :StaticMerger
|
|
37
|
+
use(:Generator) { options[:generator] }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ORB
|
|
4
|
+
module Temple
|
|
5
|
+
class Filters < ::Temple::Filter
|
|
6
|
+
def initialize(options = {})
|
|
7
|
+
@options = options
|
|
8
|
+
@attributes_compiler = AttributesCompiler.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Handle an HTML tag expression `[:orb, :tag, name, attributes, content]`
|
|
12
|
+
#
|
|
13
|
+
# @param [String] name The name of the tag
|
|
14
|
+
# @param [Array] attributes The attributes to be passed in to the tag
|
|
15
|
+
# @param [Array] content (optional) child nodes of the tag
|
|
16
|
+
# @return [Array] compiled Temple core expression
|
|
17
|
+
def on_orb_tag(name, attributes, content = nil)
|
|
18
|
+
[:html, :tag, name, @attributes_compiler.compile_attributes(attributes), compile(content)]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Handle a component tag expression `[:orb, :component, name, attributes, content]`
|
|
22
|
+
#
|
|
23
|
+
# @param [String] name The name of the component
|
|
24
|
+
# @param [Array<ORB::AST::Attribute>] attributes The attributes to be passed in to the component
|
|
25
|
+
# @param [Array] content (optional) Temple expression
|
|
26
|
+
# @return [Array] compiled Temple core expression
|
|
27
|
+
def on_orb_component(node, content = [])
|
|
28
|
+
tmp = unique_name
|
|
29
|
+
|
|
30
|
+
# Lookup the component class name using the ORB lookup mechanism
|
|
31
|
+
# that traverses the configured namespaces
|
|
32
|
+
name = node.tag.gsub('.', '::')
|
|
33
|
+
komponent = ORB.lookup_component(name)
|
|
34
|
+
komponent_name = komponent || name
|
|
35
|
+
|
|
36
|
+
block_name = "__orb__#{komponent_name.rpartition('::').last.underscore}"
|
|
37
|
+
block_name = node.directives.fetch(:with, block_name)
|
|
38
|
+
|
|
39
|
+
# We need to compile the attributes into a set of captures and a set of arguments
|
|
40
|
+
# since arguments passed to the view component constructor may be defined as
|
|
41
|
+
# dynamic expressions in our template, and we need to first capture their results.
|
|
42
|
+
arg_captures = @attributes_compiler.compile_captures(node.attributes, tmp)
|
|
43
|
+
args = @attributes_compiler.compile_komponent_args(node.attributes, tmp)
|
|
44
|
+
|
|
45
|
+
# Construct the render call for the view component
|
|
46
|
+
code = "render #{komponent_name}.new(#{args}) do |#{block_name}|"
|
|
47
|
+
|
|
48
|
+
# Return a compiled Temple expression that captures the component render call
|
|
49
|
+
# and then evaluates the result into the OutputBuffer.
|
|
50
|
+
[:multi,
|
|
51
|
+
*arg_captures,
|
|
52
|
+
# Capture the result of the component render call into a variable
|
|
53
|
+
# we can't do :dynamic here because it's probably not a complete expression
|
|
54
|
+
[:block, "#{tmp} = #{code}",
|
|
55
|
+
# Capture the content of the block into a separate buffer
|
|
56
|
+
# [:capture, unique_name, compile(content)]
|
|
57
|
+
compile(content)],
|
|
58
|
+
# Output the content
|
|
59
|
+
[:escape, true, [:dynamic, tmp.to_s]]]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Handle a component slot tag expression `[:orb, :slot, name, attributes, content]`
|
|
63
|
+
#
|
|
64
|
+
# @param [String] name The name of the slot
|
|
65
|
+
# @param [Array<ORB::AST::Attribute>] attributes the attributes to be passed in to the slot
|
|
66
|
+
# @param [Array] content (optional) Temple expression for the slot content
|
|
67
|
+
# @return [Array] compiled Temple expression
|
|
68
|
+
def on_orb_slot(node, content = [])
|
|
69
|
+
tmp = unique_name
|
|
70
|
+
|
|
71
|
+
# We need to compile the attributes into a set of captures and a set of arguments
|
|
72
|
+
# since arguments passed to the view component constructor may be defined as
|
|
73
|
+
# dynamic expressions in our template, and we need to first capture their results.
|
|
74
|
+
arg_captures = @attributes_compiler.compile_captures(node.attributes, tmp)
|
|
75
|
+
args = @attributes_compiler.compile_komponent_args(node.attributes, tmp)
|
|
76
|
+
|
|
77
|
+
# Prepare the slot name, parent name, and block name
|
|
78
|
+
slot_name = node.slot
|
|
79
|
+
parent_name = "__orb__#{node.component.underscore}"
|
|
80
|
+
block_name = node.directives.fetch(:with, "__orb__#{slot_name}")
|
|
81
|
+
|
|
82
|
+
# Construct the code to call the slot on the parent component
|
|
83
|
+
code = "#{parent_name}.with_#{slot_name}(#{args}) do |#{block_name}|"
|
|
84
|
+
|
|
85
|
+
# Return a compiled Temple expression that captures the slot call
|
|
86
|
+
[:multi,
|
|
87
|
+
*arg_captures,
|
|
88
|
+
[:code, code], compile(content),
|
|
89
|
+
[:code, "end"]]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Handle an if block expression `[:orb, :if, condition, yes, no]`
|
|
93
|
+
#
|
|
94
|
+
# @param [String] condition The condition to be evaluated
|
|
95
|
+
# @param [Array] yes The content to be rendered if the condition is true
|
|
96
|
+
# @param [Array] no (optional) The content to be rendered if the condition is false
|
|
97
|
+
# @return [Array] compiled Temple expression
|
|
98
|
+
def on_orb_if(condition, yes, no = nil)
|
|
99
|
+
result = [:if, condition, compile(yes)]
|
|
100
|
+
result << compile(no) if no
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Handle a for block expression `[:orb, :for, expression, content]`
|
|
105
|
+
#
|
|
106
|
+
# @param [String] expression The iterator expression to be evaluated
|
|
107
|
+
# @param [Array] content The content to be rendered for each iteration
|
|
108
|
+
# @return [Array] compiled Temple expression
|
|
109
|
+
def on_orb_for(expression, content)
|
|
110
|
+
enumerator, collection = expression.split(' in ')
|
|
111
|
+
code = "#{collection}.each do |#{enumerator}|"
|
|
112
|
+
|
|
113
|
+
[:multi,
|
|
114
|
+
[:code, code], compile(content),
|
|
115
|
+
[:code, "end"]]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Handle a dynamic node expression `[:orb, :dynamic, node, content]`
|
|
119
|
+
#
|
|
120
|
+
def on_orb_dynamic(node, content)
|
|
121
|
+
# TODO: Determine whether the node is an html_tag, component, or slot node
|
|
122
|
+
tmp = unique_name
|
|
123
|
+
splats = @attributes_compiler.compile_splat_attributes(node.splat_attributes)
|
|
124
|
+
code = "content_tag('#{node.tag}', #{splats}) do"
|
|
125
|
+
|
|
126
|
+
[:multi,
|
|
127
|
+
[:block, "#{tmp} = #{code}", compile(content)],
|
|
128
|
+
[:escape, true, [:dynamic, tmp]]]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# TODO: This class is a WIP and not used in production code, yet.
|
|
4
|
+
#
|
|
5
|
+
# Eventually this class will be used to generate the Ruby code from the AST.
|
|
6
|
+
# It tries to match the behaviour of the rails ERB Template handler as close as possible.
|
|
7
|
+
module ORB
|
|
8
|
+
module Temple
|
|
9
|
+
module Generators
|
|
10
|
+
class Generator
|
|
11
|
+
include Utils
|
|
12
|
+
include Mixins::CompiledDispatcher
|
|
13
|
+
include Mixins::Options
|
|
14
|
+
|
|
15
|
+
define_options :save_buffer,
|
|
16
|
+
:streaming,
|
|
17
|
+
capture_generator: 'Generator',
|
|
18
|
+
buffer_class: 'ActionView::OutputBuffer',
|
|
19
|
+
buffer: '@output_buffer',
|
|
20
|
+
freeze_static: true
|
|
21
|
+
|
|
22
|
+
def call(exp)
|
|
23
|
+
[preamble, compile(exp), postamble].flatten.compact.join('; ')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def preamble
|
|
27
|
+
[save_buffer, create_buffer]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def postamble
|
|
31
|
+
[return_buffer, restore_buffer]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def save_buffer
|
|
35
|
+
"begin; #{@original_buffer = unique_name} = #{buffer} if defined?(#{buffer})" if options[:save_buffer]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def restore_buffer
|
|
39
|
+
"ensure; #{buffer} = #{@original_buffer}; end" if options[:save_buffer]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create_buffer
|
|
43
|
+
if buffer == '@output_buffer'
|
|
44
|
+
"#{buffer} = output_buffer || #{options[:buffer_class]}.new"
|
|
45
|
+
else
|
|
46
|
+
"#{buffer} = #{options[:buffer_class]}.new"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def return_buffer
|
|
51
|
+
'nil'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on(*exp)
|
|
55
|
+
raise InvalidExpression, "Generator supports only core expressions - found #{exp.inspect}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def on_multi(*exp)
|
|
59
|
+
exp.map { |e| compile(e) }.join('; ')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_newline
|
|
63
|
+
"\n"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def on_capture(name, exp)
|
|
67
|
+
capture_generator.new(capture_generator: options[:capture_generator],
|
|
68
|
+
freeze_static: options[:freeze_static],
|
|
69
|
+
buffer: name).call(exp)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def on_static(text)
|
|
73
|
+
concat(options[:freeze_static] ? "#{text.inspect}.freeze" : text.inspect)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def on_dynamic(code)
|
|
77
|
+
concat(code)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def on_code(code)
|
|
81
|
+
code
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
protected
|
|
85
|
+
|
|
86
|
+
def buffer
|
|
87
|
+
options[:buffer]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def capture_generator
|
|
91
|
+
@capture_generator ||= if Class === options[:capture_generator]
|
|
92
|
+
options[:capture_generator]
|
|
93
|
+
else
|
|
94
|
+
Generators.const_get(options[:capture_generator])
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def safe_concat(str)
|
|
99
|
+
"#{buffer}.safe_append=#{str}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def concat(str)
|
|
103
|
+
"#{buffer}.append=#{str}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ORB
|
|
4
|
+
module Temple
|
|
5
|
+
class Identity
|
|
6
|
+
def initialize
|
|
7
|
+
@unique_id = 0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def generate(prefix = nil)
|
|
11
|
+
@unique_id += 1
|
|
12
|
+
["_orb_compiler", prefix, @unique_id].compact.join('_')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ORB
|
|
4
|
+
module Temple
|
|
5
|
+
class Parser
|
|
6
|
+
def initialize(options = {})
|
|
7
|
+
@options = options
|
|
8
|
+
|
|
9
|
+
# Only raise errors when is used as a standalone library
|
|
10
|
+
# and not within the Temple engine
|
|
11
|
+
@raise_errors = !options.key?(:generator)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(template)
|
|
15
|
+
tokenizer = ORB::Tokenizer2.new(template, **@options)
|
|
16
|
+
tokens = tokenizer.tokenize!
|
|
17
|
+
|
|
18
|
+
parser = ORB::Parser.new(tokens, **@options)
|
|
19
|
+
@root = parser.parse!
|
|
20
|
+
|
|
21
|
+
@root
|
|
22
|
+
rescue ORB::Error => e
|
|
23
|
+
e.set_backtrace(e.backtrace.unshift("#{@options.fetch(:filename, :nofile)}:#{e.line || 42}"))
|
|
24
|
+
|
|
25
|
+
# Within the Temple engine, tokenizer and parser errors shouldn't be raised
|
|
26
|
+
# but instead passed on to the compiler, so we can raise them there and
|
|
27
|
+
# render nice error templates.
|
|
28
|
+
raise if @raise_errors
|
|
29
|
+
|
|
30
|
+
error_with_lineno(e)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def error_with_lineno(error)
|
|
36
|
+
return error if error.line
|
|
37
|
+
|
|
38
|
+
trace = error.backtrace.first
|
|
39
|
+
return error unless trace
|
|
40
|
+
|
|
41
|
+
line = trace.match(/\d+\z/).to_s.to_i
|
|
42
|
+
Error.new(error.message, line)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/orb/temple.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ORB
|
|
4
|
+
module Temple
|
|
5
|
+
extend ActiveSupport::Autoload
|
|
6
|
+
|
|
7
|
+
autoload :Filters
|
|
8
|
+
autoload :ComponentFilter
|
|
9
|
+
autoload :AttributesCompiler
|
|
10
|
+
autoload :Parser
|
|
11
|
+
autoload :Compiler
|
|
12
|
+
autoload :Identity
|
|
13
|
+
autoload :Engine
|
|
14
|
+
autoload :Generator, 'orb/temple/generators'
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/orb/token.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ORB
|
|
4
|
+
# A simple PORO to represent a token.
|
|
5
|
+
class Token
|
|
6
|
+
attr_accessor :type, :value, :meta
|
|
7
|
+
|
|
8
|
+
def initialize(type, value, meta = {})
|
|
9
|
+
@type = type
|
|
10
|
+
@value = value
|
|
11
|
+
@meta = meta
|
|
12
|
+
@line = line || 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def set_meta(key, value)
|
|
16
|
+
meta[key.to_sym] = value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def respond_to_missing?(method, _include_private = false)
|
|
20
|
+
meta.has_key?(method.to_sym)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def method_missing(method, *args, &block)
|
|
24
|
+
if meta.has_key?(method.to_sym)
|
|
25
|
+
meta[method.to_sym]
|
|
26
|
+
else
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(other)
|
|
32
|
+
type == other.type &&
|
|
33
|
+
value == other.value &&
|
|
34
|
+
meta == other.meta
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :line
|
|
38
|
+
|
|
39
|
+
def inspect
|
|
40
|
+
to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_s
|
|
44
|
+
"#<ORB::Token[#{type}] meta=#{meta.inspect} value=#{value.inspect}>"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|