rbexy 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.
@@ -0,0 +1,142 @@
1
+ module Rbexy
2
+ module Nodes
3
+ module Util
4
+ def self.safe_string(str)
5
+ str.gsub('"', '\\"')
6
+ end
7
+
8
+ def self.safe_tag_name(name)
9
+ name.gsub(".", "__")
10
+ end
11
+ end
12
+
13
+ class Template
14
+ attr_reader :children
15
+
16
+ def initialize(children)
17
+ @children = children
18
+ end
19
+
20
+ def compile
21
+ [
22
+ "Rbexy::OutputBuffer.new.tap { |output|",
23
+ children.map(&:compile).map { |c| "output << (#{c})"}.join(";"),
24
+ "}.html_safe"
25
+ ].join(" ")
26
+ end
27
+ end
28
+
29
+ class Text
30
+ attr_reader :content
31
+
32
+ def initialize(content)
33
+ @content = content
34
+ end
35
+
36
+ def compile
37
+ "\"#{Util.safe_string(content)}\""
38
+ end
39
+ end
40
+
41
+ class ExpressionGroup
42
+ attr_reader :statements
43
+
44
+ def initialize(statements)
45
+ @statements = statements
46
+ end
47
+
48
+ def compile
49
+ statements.map(&:compile).join
50
+ end
51
+ end
52
+
53
+ class Expression
54
+ attr_reader :content
55
+
56
+ def initialize(content)
57
+ @content = content
58
+ end
59
+
60
+ def compile
61
+ content
62
+ end
63
+ end
64
+
65
+ class XmlNode
66
+ attr_reader :name, :members, :children
67
+
68
+ def initialize(name, members, children)
69
+ @name = name
70
+ @members = members || []
71
+ @children = children
72
+ end
73
+
74
+ def compile
75
+ base_tag = "rbexy_tag.#{Util.safe_tag_name(name)}(#{compile_members})"
76
+ tag = if children.length > 0
77
+ [
78
+ "#{base_tag} {",
79
+ "Rbexy::OutputBuffer.new.tap { |output|",
80
+ children.map(&:compile).map { |c| "output << (#{c})"}.join(";"),
81
+ "}.html_safe",
82
+ "}"
83
+ ].join(" ")
84
+ else
85
+ base_tag
86
+ end
87
+
88
+ [
89
+ "Rbexy::OutputBuffer.new.tap { |output|",
90
+ "rbexy_context.push({}) if defined?(Rbexy::Component) && self.is_a?(Rbexy::Component);",
91
+ "output << (#{tag});",
92
+ "rbexy_context.pop if defined?(Rbexy::Component) && self.is_a?(Rbexy::Component);",
93
+ "}.html_safe"
94
+ ].join(" ")
95
+ end
96
+
97
+ def compile_members
98
+ members.each_with_object("") do |member, result|
99
+ case member
100
+ when ExpressionGroup
101
+ result << "**#{member.compile},"
102
+ when SilentNewline
103
+ result << member.compile
104
+ else
105
+ result << "#{member.compile},"
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ class XmlAttr
112
+ attr_reader :name, :value
113
+
114
+ def initialize(name, value)
115
+ @name = name
116
+ @value = value
117
+ end
118
+
119
+ def compile
120
+ "\"#{name}\": #{value.compile}"
121
+ end
122
+ end
123
+
124
+ class SilentNewline
125
+ def compile
126
+ "\n"
127
+ end
128
+ end
129
+
130
+ class Declaration
131
+ attr_reader :content
132
+
133
+ def initialize(content)
134
+ @content = content
135
+ end
136
+
137
+ def compile
138
+ "\"#{Util.safe_string(content)}\".html_safe"
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,8 @@
1
+ module Rbexy
2
+ class OutputBuffer < String
3
+ def <<(content)
4
+ value = content.is_a?(Array) ? content.join : content
5
+ super([nil, false].include?(value) ? "" : value.to_s)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,204 @@
1
+ module Rbexy
2
+ class Parser
3
+ class ParseError < StandardError; end
4
+
5
+ attr_reader :tokens
6
+ attr_accessor :position
7
+
8
+ def initialize(tokens)
9
+ @tokens = tokens
10
+ @position = 0
11
+ end
12
+
13
+ def parse
14
+ validate_tokens!
15
+ Nodes::Template.new(parse_tokens)
16
+ end
17
+
18
+ def parse_tokens
19
+ results = []
20
+
21
+ while result = parse_token
22
+ results << result
23
+ end
24
+
25
+ results
26
+ end
27
+
28
+ def parse_token
29
+ parse_text || parse_silent_newline || parse_expression || parse_tag || parse_declaration
30
+ end
31
+
32
+ def parse_text
33
+ return unless token = take(:TEXT)
34
+ Nodes::Text.new(token[1])
35
+ end
36
+
37
+ def parse_expression
38
+ return unless take(:OPEN_EXPRESSION)
39
+
40
+ statements = []
41
+
42
+ eventually!(:CLOSE_EXPRESSION)
43
+ until take(:CLOSE_EXPRESSION)
44
+ statements << (parse_expression_body || parse_tag)
45
+ end
46
+
47
+ Nodes::ExpressionGroup.new(statements)
48
+ end
49
+
50
+ def parse_expression!
51
+ peek!(:OPEN_EXPRESSION)
52
+ parse_expression
53
+ end
54
+
55
+ def parse_expression_body
56
+ return unless token = take(:EXPRESSION_BODY)
57
+ Nodes::Expression.new(token[1])
58
+ end
59
+
60
+ def parse_tag
61
+ return unless take(:OPEN_TAG_DEF)
62
+
63
+ name = take!(:TAG_NAME)
64
+ members = []
65
+ members.concat(take_all(:SILENT_NEWLINE).map { Nodes::SilentNewline.new })
66
+ members.concat(parse_attrs)
67
+
68
+ take!(:CLOSE_TAG_DEF)
69
+
70
+ children = parse_children
71
+
72
+ Nodes::XmlNode.new(name[1], members, children)
73
+ end
74
+
75
+ def parse_attrs
76
+ return [] unless take(:OPEN_ATTRS)
77
+
78
+ attrs = []
79
+
80
+ eventually!(:CLOSE_ATTRS)
81
+ until take(:CLOSE_ATTRS)
82
+ attrs << (parse_splat_attr || parse_silent_newline || parse_attr)
83
+ end
84
+
85
+ attrs
86
+ end
87
+
88
+ def parse_splat_attr
89
+ return unless take(:OPEN_ATTR_SPLAT)
90
+
91
+ expression = parse_expression!
92
+ take!(:CLOSE_ATTR_SPLAT)
93
+
94
+ expression
95
+ end
96
+
97
+ def parse_silent_newline
98
+ return unless take(:SILENT_NEWLINE)
99
+ Nodes::SilentNewline.new
100
+ end
101
+
102
+ def parse_attr
103
+ name = take!(:ATTR_NAME)[1]
104
+ value = nil
105
+
106
+ if take(:OPEN_ATTR_VALUE)
107
+ value = parse_text || parse_expression
108
+ raise ParseError, "Missing attribute value" unless value
109
+ take(:CLOSE_ATTR_VALUE)
110
+ else
111
+ value = default_empty_attr_value
112
+ end
113
+
114
+ Nodes::XmlAttr.new(name, value)
115
+ end
116
+
117
+ def parse_children
118
+ children = []
119
+
120
+ eventually!(:OPEN_TAG_END)
121
+ until take(:OPEN_TAG_END)
122
+ children << parse_token
123
+ end
124
+
125
+ take(:TAG_NAME)
126
+ take!(:CLOSE_TAG_END)
127
+
128
+ children
129
+ end
130
+
131
+ private
132
+
133
+ def parse_declaration
134
+ return unless token = take(:DECLARATION)
135
+ Nodes::Declaration.new(token[1])
136
+ end
137
+
138
+ def take(token_name)
139
+ if token = peek(token_name)
140
+ self.position += 1
141
+ token
142
+ end
143
+ end
144
+
145
+ def take_all(token_name)
146
+ result = []
147
+ while token = take(token_name)
148
+ result << token
149
+ end
150
+ result
151
+ end
152
+
153
+ def take!(token_name)
154
+ take(token_name) || unexpected_token!(token_name)
155
+ end
156
+
157
+ def peek(token_name)
158
+ if (token = tokens[position]) && token[0] == token_name
159
+ token
160
+ end
161
+ end
162
+
163
+ def peek!(token_name)
164
+ peek(token_name) || unexpected_token!(token_name)
165
+ end
166
+
167
+ def eventually!(token_name)
168
+ tokens[position..-1].first { |t| t[0] == token_name } ||
169
+ raise(ParseError, "Expected to find a #{token_name} but never did")
170
+ end
171
+
172
+ def default_empty_attr_value
173
+ Nodes::Text.new("")
174
+ end
175
+
176
+ def error_window
177
+ window_start = position - 2
178
+ window_start = 0 if window_start < 0
179
+ window_end = position + 2
180
+ window_end = tokens.length-1 if window_end >= tokens.length
181
+
182
+ tokens[window_start..window_end].map.with_index do |token, i|
183
+ prefix = window_start + i == position ? "=>" : " "
184
+ "#{prefix} #{token}"
185
+ end.join("\n")
186
+ end
187
+
188
+ def unexpected_token!(expected_token)
189
+ raise(ParseError, "Unexpected token #{tokens[position][0]}, expecting #{expected_token}\n#{error_window}")
190
+ end
191
+
192
+ def validate_tokens!
193
+ validate_all_tags_close!
194
+ end
195
+
196
+ def validate_all_tags_close!
197
+ open_count = tokens.count { |t| t[0] == :OPEN_TAG_DEF }
198
+ close_count = tokens.count { |t| t[0] == :OPEN_TAG_END }
199
+ if open_count != close_count
200
+ raise(ParseError, "#{open_count - close_count} tags fail to close. All tags must close, either <NAME></NAME> or self-closing <NAME />")
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,8 @@
1
+ module Rbexy
2
+ autoload :Component, "rbexy/component"
3
+
4
+ module Rails
5
+ autoload :TemplateHandler, "rbexy/rails/template_handler"
6
+ autoload :Engine, "rbexy/rails/engine"
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ require "rbexy/rails"
2
+
3
+ module Rbexy
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ initializer "rbexy" do |app|
7
+ ActionView::Template.register_template_handler(:rbx, Rbexy::Rails::TemplateHandler)
8
+
9
+ ActiveSupport.on_load :action_controller do
10
+ helper Rbexy::ViewContextHelper
11
+ end
12
+
13
+ if defined?(ViewComponent)
14
+ ViewComponent::Base.include Rbexy::ViewContextHelper
15
+ end
16
+
17
+ Rbexy.configure do |config|
18
+ require "rbexy/component_providers/rbexy_provider"
19
+ config.component_provider = Rbexy::ComponentProviders::RbexyProvider.new
20
+ config.template_paths << ::Rails.root.join("app", "components")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module Rbexy
2
+ module Rails
3
+ class TemplateHandler
4
+ def self.call(template, source)
5
+ Rbexy.compile(source)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ require "active_support/all"
2
+ require "action_view/helpers"
3
+ require "action_view/context"
4
+ require "action_view/buffers"
5
+
6
+ module Rbexy
7
+ class Runtime
8
+ include ActionView::Context
9
+ include ActionView::Helpers::TagHelper
10
+ include ViewContextHelper
11
+
12
+ DefaultTagBuilder = ActionView::Helpers::TagHelper::TagBuilder
13
+
14
+ def self.create_tag_builder(context, provider = Rbexy.configuration.component_provider)
15
+ if provider
16
+ ComponentTagBuilder.new(context, provider)
17
+ else
18
+ ActionView::Helpers::TagHelper::TagBuilder.new(context)
19
+ end
20
+ end
21
+
22
+ def initialize(component_provider = nil)
23
+ @rbexy_tag = self.class.create_tag_builder(self, component_provider)
24
+ end
25
+
26
+ def evaluate(code)
27
+ instance_eval(code)
28
+ rescue => e
29
+ e.set_backtrace(e.backtrace.map { |l| l.gsub("(eval)", "(rbx template string)") })
30
+ raise e
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Rbexy
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,11 @@
1
+ module Rbexy
2
+ module ViewContextHelper
3
+ def rbexy_tag
4
+ @rbexy_tag ||= Runtime.create_tag_builder(self)
5
+ end
6
+
7
+ def rbexy_context
8
+ @rbexy_context ||= [{}]
9
+ end
10
+ end
11
+ end