brut 0.0.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 +7 -0
- data/.gitignore +7 -0
- data/CODE_OF_CONDUCT.txt +99 -0
- data/Dockerfile.dx +32 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +370 -0
- data/README.md +21 -0
- data/Rakefile +1 -0
- data/bin/bin_kit.rb +39 -0
- data/bin/rake +27 -0
- data/bin/setup +145 -0
- data/brut.gemspec +60 -0
- data/docker-compose.dx.yml +16 -0
- data/dx/build +26 -0
- data/dx/docker-compose.env +22 -0
- data/dx/dx.sh.lib +24 -0
- data/dx/exec +58 -0
- data/dx/prune +19 -0
- data/dx/setupkit.sh.lib +144 -0
- data/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/dx/start +30 -0
- data/dx/stop +23 -0
- data/lib/brut/back_end/action.rb +3 -0
- data/lib/brut/back_end/result.rb +46 -0
- data/lib/brut/back_end/seed_data.rb +24 -0
- data/lib/brut/back_end/validator.rb +3 -0
- data/lib/brut/back_end/validators/form_validator.rb +37 -0
- data/lib/brut/cli/app.rb +130 -0
- data/lib/brut/cli/app_runner.rb +219 -0
- data/lib/brut/cli/apps/build_assets.rb +123 -0
- data/lib/brut/cli/apps/db.rb +279 -0
- data/lib/brut/cli/apps/scaffold.rb +256 -0
- data/lib/brut/cli/apps/test.rb +200 -0
- data/lib/brut/cli/command.rb +130 -0
- data/lib/brut/cli/error.rb +12 -0
- data/lib/brut/cli/execution_results.rb +81 -0
- data/lib/brut/cli/executor.rb +37 -0
- data/lib/brut/cli/options.rb +46 -0
- data/lib/brut/cli/output.rb +30 -0
- data/lib/brut/cli.rb +24 -0
- data/lib/brut/factory_bot.rb +20 -0
- data/lib/brut/framework/app.rb +55 -0
- data/lib/brut/framework/config.rb +415 -0
- data/lib/brut/framework/container.rb +190 -0
- data/lib/brut/framework/errors/abstract_method.rb +9 -0
- data/lib/brut/framework/errors/bug.rb +14 -0
- data/lib/brut/framework/errors/not_found.rb +10 -0
- data/lib/brut/framework/errors.rb +14 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
- data/lib/brut/framework/mcp.rb +215 -0
- data/lib/brut/framework/project_environment.rb +18 -0
- data/lib/brut/framework.rb +13 -0
- data/lib/brut/front_end/asset_metadata.rb +76 -0
- data/lib/brut/front_end/component.rb +213 -0
- data/lib/brut/front_end/components/form_tag.rb +71 -0
- data/lib/brut/front_end/components/i18n_translations.rb +36 -0
- data/lib/brut/front_end/components/input.rb +13 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
- data/lib/brut/front_end/components/inputs/select.rb +100 -0
- data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
- data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
- data/lib/brut/front_end/components/locale_detection.rb +25 -0
- data/lib/brut/front_end/components/page_identifier.rb +13 -0
- data/lib/brut/front_end/components/timestamp.rb +33 -0
- data/lib/brut/front_end/download.rb +23 -0
- data/lib/brut/front_end/flash.rb +57 -0
- data/lib/brut/front_end/form.rb +171 -0
- data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
- data/lib/brut/front_end/forms/input.rb +119 -0
- data/lib/brut/front_end/forms/input_definition.rb +100 -0
- data/lib/brut/front_end/forms/validity_state.rb +36 -0
- data/lib/brut/front_end/handler.rb +48 -0
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
- data/lib/brut/front_end/handling_results.rb +14 -0
- data/lib/brut/front_end/http_method.rb +33 -0
- data/lib/brut/front_end/http_status.rb +16 -0
- data/lib/brut/front_end/middleware.rb +7 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
- data/lib/brut/front_end/page.rb +47 -0
- data/lib/brut/front_end/request_context.rb +82 -0
- data/lib/brut/front_end/route_hook.rb +15 -0
- data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
- data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
- data/lib/brut/front_end/routing.rb +236 -0
- data/lib/brut/front_end/session.rb +56 -0
- data/lib/brut/front_end/template.rb +32 -0
- data/lib/brut/front_end/templates/block_filter.rb +60 -0
- data/lib/brut/front_end/templates/erb_engine.rb +26 -0
- data/lib/brut/front_end/templates/erb_parser.rb +84 -0
- data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
- data/lib/brut/i18n/base_methods.rb +168 -0
- data/lib/brut/i18n/for_cli.rb +4 -0
- data/lib/brut/i18n/for_html.rb +4 -0
- data/lib/brut/i18n/http_accept_language.rb +68 -0
- data/lib/brut/i18n.rb +6 -0
- data/lib/brut/instrumentation/basic.rb +66 -0
- data/lib/brut/instrumentation/event.rb +19 -0
- data/lib/brut/instrumentation/http_event.rb +5 -0
- data/lib/brut/instrumentation/subscriber.rb +41 -0
- data/lib/brut/instrumentation.rb +11 -0
- data/lib/brut/junk_drawer.rb +88 -0
- data/lib/brut/sinatra_helpers.rb +183 -0
- data/lib/brut/spec_support/component_support.rb +49 -0
- data/lib/brut/spec_support/flash_support.rb +7 -0
- data/lib/brut/spec_support/general_support.rb +18 -0
- data/lib/brut/spec_support/handler_support.rb +7 -0
- data/lib/brut/spec_support/matcher.rb +9 -0
- data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
- data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
- data/lib/brut/spec_support/session_support.rb +3 -0
- data/lib/brut/spec_support.rb +12 -0
- data/lib/brut/version.rb +3 -0
- data/lib/brut.rb +38 -0
- data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
- data/lib/sequel/extensions/brut_migrations.rb +98 -0
- data/lib/sequel/plugins/created_at.rb +14 -0
- data/lib/sequel/plugins/external_id.rb +45 -0
- data/lib/sequel/plugins/find_bang.rb +13 -0
- data/lib/sequel/plugins.rb +3 -0
- metadata +484 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
# Holds the registered routes for this app.
|
|
4
|
+
class Brut::FrontEnd::Routing
|
|
5
|
+
|
|
6
|
+
include SemanticLogger::Loggable
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@routes = Set.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def for(path:,method:)
|
|
13
|
+
http_method = Brut::FrontEnd::HttpMethod.new(method)
|
|
14
|
+
@routes.detect { |route|
|
|
15
|
+
route.path_template == path &&
|
|
16
|
+
route.http_method == http_method
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reload
|
|
21
|
+
new_routes = @routes.map { |route|
|
|
22
|
+
if route.class == Route
|
|
23
|
+
route.class.new(route.http_method,route.path_template)
|
|
24
|
+
else
|
|
25
|
+
route.class.new(route.path_template)
|
|
26
|
+
end
|
|
27
|
+
}
|
|
28
|
+
@routes = Set.new(new_routes)
|
|
29
|
+
@routes.each do |route|
|
|
30
|
+
handler_class = route.handler_class
|
|
31
|
+
if handler_class.name !~ /^Brut::[A-Z]/
|
|
32
|
+
add_routing_method(route)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def register_page(path)
|
|
38
|
+
route = PageRoute.new(path)
|
|
39
|
+
@routes << route
|
|
40
|
+
add_routing_method(route)
|
|
41
|
+
route
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def register_form(path)
|
|
45
|
+
route = FormRoute.new(path)
|
|
46
|
+
@routes << route
|
|
47
|
+
add_routing_method(route)
|
|
48
|
+
route
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def register_handler_only(path)
|
|
52
|
+
route = FormHandlerRoute.new(path)
|
|
53
|
+
@routes << route
|
|
54
|
+
add_routing_method(route)
|
|
55
|
+
route
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def register_path(path, method:)
|
|
59
|
+
route = Route.new(method, path)
|
|
60
|
+
@routes << route
|
|
61
|
+
add_routing_method(route)
|
|
62
|
+
route
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def route(handler_class)
|
|
66
|
+
route = @routes.detect { |route|
|
|
67
|
+
handler_class_match = route.handler_class.name == handler_class.name
|
|
68
|
+
form_class_match = if route.respond_to?(:form_class)
|
|
69
|
+
route.form_class.name == handler_class.name
|
|
70
|
+
else
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
handler_class_match || form_class_match
|
|
74
|
+
}
|
|
75
|
+
if !route
|
|
76
|
+
raise ArgumentError,"There is no configured route for #{handler_class}"
|
|
77
|
+
end
|
|
78
|
+
route
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def uri(handler_class, with_method: :any, **rest)
|
|
82
|
+
route = self.route(handler_class)
|
|
83
|
+
route_allowed_for_method = if with_method == :any
|
|
84
|
+
true
|
|
85
|
+
elsif Brut::FrontEnd::HttpMethod.new(with_method) == route.http_method
|
|
86
|
+
true
|
|
87
|
+
else
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
if !route_allowed_for_method
|
|
91
|
+
raise ArgumentError,"The route for '#{handler_class}' (#{route.path}) is not supported by HTTP method '#{with_method}'"
|
|
92
|
+
end
|
|
93
|
+
route.path(**rest)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def inspect
|
|
97
|
+
@routes.map { |route|
|
|
98
|
+
"#{route.http_method}:#{route.path_template} - #{route.handler_class.name}"
|
|
99
|
+
}.join("\n")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_routing_method(route)
|
|
103
|
+
handler_class = route.handler_class
|
|
104
|
+
if handler_class.respond_to?(:routing) && handler_class.method(:routing).owner != Brut::FrontEnd::Form
|
|
105
|
+
raise ArgumentError,"#{handler_class} (that handles path #{route.path_template}) got it's ::routing method from #{handler_class.method(:routing).owner}, meaning it has overridden the value fro Brut::FrontEnd::Form"
|
|
106
|
+
end
|
|
107
|
+
form_class = route.respond_to?(:form_class) ? route.form_class : nil
|
|
108
|
+
[ handler_class, form_class ].compact.each do |klass|
|
|
109
|
+
klass.class_eval do
|
|
110
|
+
def self.routing(**args)
|
|
111
|
+
Brut.container.routing.uri(self,**args)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class Route
|
|
118
|
+
|
|
119
|
+
include SemanticLogger::Loggable
|
|
120
|
+
|
|
121
|
+
attr_reader :handler_class, :path_template, :http_method
|
|
122
|
+
|
|
123
|
+
def initialize(method,path_template)
|
|
124
|
+
http_method = Brut::FrontEnd::HttpMethod.new(method)
|
|
125
|
+
if ![:get, :post].include?(http_method.to_sym)
|
|
126
|
+
raise ArgumentError,"Only GET and POST are supported. '#{method}' is not"
|
|
127
|
+
end
|
|
128
|
+
if path_template !~ /^\//
|
|
129
|
+
raise ArgumentError,"Routes must start with a slash: '#{path_template}'"
|
|
130
|
+
end
|
|
131
|
+
@http_method = http_method
|
|
132
|
+
@path_template = path_template
|
|
133
|
+
@handler_class = self.locate_handler_class(self.suffix,self.preposition)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def path(**query_string_params)
|
|
137
|
+
path = @path_template.split(/\//).map { |path_part|
|
|
138
|
+
if path_part =~ /^:(.+)$/
|
|
139
|
+
param_name = $1.to_sym
|
|
140
|
+
if !query_string_params.key?(param_name)
|
|
141
|
+
query_string_params_for_message = if query_string_params.keys.any?
|
|
142
|
+
query_string_params.keys.map(&:to_s).join(", ")
|
|
143
|
+
else
|
|
144
|
+
"no params"
|
|
145
|
+
end
|
|
146
|
+
raise ArgumentError,"path for #{@handler_class} requires '#{param_name}' as a path parameter, but it was not specified to #path. Got #{query_string_params_for_message}"
|
|
147
|
+
end
|
|
148
|
+
query_string_params.delete(param_name)
|
|
149
|
+
else
|
|
150
|
+
path_part
|
|
151
|
+
end
|
|
152
|
+
}
|
|
153
|
+
uri = URI(path.join("/"))
|
|
154
|
+
uri.query = URI.encode_www_form(query_string_params)
|
|
155
|
+
uri
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def ==(other)
|
|
159
|
+
self.method == other.method && self.path == other.path
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
def locate_handler_class(suffix,preposition, on_missing: :raise)
|
|
164
|
+
if @path_template == "/"
|
|
165
|
+
return Module.const_get("HomePage")
|
|
166
|
+
end
|
|
167
|
+
path_parts = @path_template.split(/\//)[1..-1]
|
|
168
|
+
|
|
169
|
+
part_names = path_parts.reduce([]) { |array,path_part|
|
|
170
|
+
if path_part =~ /^:(.+)$/
|
|
171
|
+
if array.empty?
|
|
172
|
+
raise ArgumentError,"Your path may not start with a placeholder: '#{@path_template}'"
|
|
173
|
+
end
|
|
174
|
+
placeholder_camelized = RichString.new($1).camelize
|
|
175
|
+
array[-1] << preposition
|
|
176
|
+
array[-1] << placeholder_camelized.to_s
|
|
177
|
+
elsif array.empty? && path_part == "__brut"
|
|
178
|
+
array << "Brut"
|
|
179
|
+
array << "FrontEnd"
|
|
180
|
+
array << "Handlers"
|
|
181
|
+
else
|
|
182
|
+
array << RichString.new(path_part).camelize.to_s
|
|
183
|
+
end
|
|
184
|
+
array
|
|
185
|
+
}
|
|
186
|
+
part_names[-1] += suffix
|
|
187
|
+
part_names.inject(Module) { |mod,path_element|
|
|
188
|
+
mod.const_get(path_element,mod == Module)
|
|
189
|
+
}
|
|
190
|
+
rescue NameError => ex
|
|
191
|
+
if on_missing == :raise
|
|
192
|
+
module_message = if ex.receiver == Module
|
|
193
|
+
"Could not find"
|
|
194
|
+
else
|
|
195
|
+
"Module '#{ex.receiver}' did not have"
|
|
196
|
+
end
|
|
197
|
+
message = "Cannot find page class for route '#{@path_template}', which should be #{part_names.join("::")}. #{module_message} the class or module '#{ex.name}'"
|
|
198
|
+
raise message
|
|
199
|
+
else
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def suffix = "Handler"
|
|
205
|
+
def preposition = "With"
|
|
206
|
+
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class PageRoute < Route
|
|
210
|
+
def initialize(path_template)
|
|
211
|
+
super(Brut::FrontEnd::HttpMethod.new(:get),path_template)
|
|
212
|
+
end
|
|
213
|
+
def suffix = "Page"
|
|
214
|
+
def preposition = "By"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
class FormRoute < Route
|
|
218
|
+
attr_reader :form_class
|
|
219
|
+
def initialize(path_template)
|
|
220
|
+
super(Brut::FrontEnd::HttpMethod.new(:post),path_template)
|
|
221
|
+
@form_class = self.locate_handler_class("Form","With")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
class FormHandlerRoute < Route
|
|
226
|
+
def initialize(path_template)
|
|
227
|
+
super(Brut::FrontEnd::HttpMethod.new(:post),path_template)
|
|
228
|
+
unnecessary_class = self.locate_handler_class("Form","With", on_missing: nil)
|
|
229
|
+
if !unnecessary_class.nil?
|
|
230
|
+
raise ArgumentError,"#{path_template} should only have #{handler_class} defined, however #{unnecessary_class} was found. If #{path_template} should be a form submission, use `form \"#{path_template}\"` instead of `action \"#{path_template}\"`. Otherwise, delete #{unnecessary_class}"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
end
|
|
236
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# A class that represents the current session, as opposed to just a Hash.
|
|
2
|
+
# Generally, this can act like a Hash for setting and accessing values stored in the session.
|
|
3
|
+
# It provides a few useful additions:
|
|
4
|
+
#
|
|
5
|
+
# * Your app can extend this to provide an app-specific API around the session.
|
|
6
|
+
# * There is direct access to commonly-used data stored in the session, such as the flash.
|
|
7
|
+
class Brut::FrontEnd::Session
|
|
8
|
+
def initialize(rack_session:)
|
|
9
|
+
@rack_session = rack_session
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def http_accept_language = Brut::I18n::HTTPAcceptLanguage.from_session(self[:__brut_http_accept_language])
|
|
13
|
+
def http_accept_language=(http_accept_language)
|
|
14
|
+
self[:__brut_http_accept_language] = http_accept_language.for_session
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Get the timezone as reported by the browser, as a TZInfo::Timezone.
|
|
18
|
+
# If none is available or the browser reported an invalid value, this returns nil.
|
|
19
|
+
def timezone_from_browser
|
|
20
|
+
tz_name = self[:__brut_timezone_from_browser]
|
|
21
|
+
if tz_name.nil?
|
|
22
|
+
return nil
|
|
23
|
+
end
|
|
24
|
+
begin
|
|
25
|
+
TZInfo::Timezone.get(tz_name)
|
|
26
|
+
rescue TZInfo::InvalidTimezoneIdentifier => ex
|
|
27
|
+
SemanticLogger[self.class.name].warn(ex)
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Set the timezone as reported by the browser. This alleviates the need to keep
|
|
33
|
+
# asking the browser for this information.
|
|
34
|
+
def timezone_from_browser=(timezone)
|
|
35
|
+
if timezone.kind_of?(TZInfo::Timezone)
|
|
36
|
+
timezone = timezone.name
|
|
37
|
+
end
|
|
38
|
+
self[:__brut_timezone_from_browser] = timezone
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def[](key) = @rack_session[key.to_s]
|
|
42
|
+
|
|
43
|
+
def[]=(key,value)
|
|
44
|
+
@rack_session[key.to_s] = value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete(key) = @rack_session.delete(key.to_s)
|
|
48
|
+
|
|
49
|
+
# Access the flash, as an instance of whatever class has been configured.
|
|
50
|
+
def flash
|
|
51
|
+
Brut.container.flash_class.from_h(self[:__brut_flash])
|
|
52
|
+
end
|
|
53
|
+
def flash=(new_flash)
|
|
54
|
+
self[:__brut_flash] = new_flash.to_h
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "temple"
|
|
2
|
+
|
|
3
|
+
module Brut::FrontEnd::Templates
|
|
4
|
+
autoload(:HTMLSafeString,"brut/front_end/templates/html_safe_string")
|
|
5
|
+
autoload(:ERBParser,"brut/front_end/templates/erb_parser")
|
|
6
|
+
autoload(:EscapableFilter,"brut/front_end/templates/escapable_filter")
|
|
7
|
+
autoload(:BlockFilter,"brut/front_end/templates/block_filter")
|
|
8
|
+
autoload(:ERBEngine,"brut/front_end/templates/erb_engine")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Handles rendering HTML templates
|
|
12
|
+
class Brut::FrontEnd::Template
|
|
13
|
+
|
|
14
|
+
TempleTemplate = Temple::Templates::Tilt(Brut::FrontEnd::Templates::ERBEngine,
|
|
15
|
+
register_as: "html.erb")
|
|
16
|
+
|
|
17
|
+
# Wraps a string that is deemed safe to insert into
|
|
18
|
+
# HTML without escaping it. This allows stuff like
|
|
19
|
+
# <%= component(SomeComponent) %> to work without
|
|
20
|
+
# having to remember to <%== all the time.
|
|
21
|
+
def initialize(template_file_path)
|
|
22
|
+
@tilt_template = Tilt.new(template_file_path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render_template(...)
|
|
26
|
+
@tilt_template.render(...)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.escape_html(string)
|
|
30
|
+
Brut::FrontEnd::Templates::EscapableFilter.escape_html(string)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# This is a slightly modified copy if Hanamis' Filters::Block:
|
|
2
|
+
#
|
|
3
|
+
# https://github.com/hanami/view/blob/main/lib/hanami/view/erb/filters/block.rb
|
|
4
|
+
#
|
|
5
|
+
class Brut::FrontEnd::Templates::BlockFilter < Temple::Filter
|
|
6
|
+
END_LINE_RE = /\bend\b/
|
|
7
|
+
|
|
8
|
+
def on_erb_block(escape, code, content)
|
|
9
|
+
tmp = unique_name
|
|
10
|
+
|
|
11
|
+
# Remove the last `end` :code sexp, since this is technically "outside" the block
|
|
12
|
+
# contents, which we want to capture separately below. This `end` is added back after
|
|
13
|
+
# capturing the content below.
|
|
14
|
+
case content.last
|
|
15
|
+
in [:code, c] if c =~ END_LINE_RE
|
|
16
|
+
content.pop
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
[:multi,
|
|
20
|
+
# Capture the result of the code in a variable. We can't do `[:dynamic, code]` because
|
|
21
|
+
# it's probably not a complete expression (which is a requirement for Temple).
|
|
22
|
+
# DBC: an example is that 'code' might be "form_for do" which is not an expression.
|
|
23
|
+
# Because we later put an "end" in, the result will be
|
|
24
|
+
#
|
|
25
|
+
# some_var = helper do
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# Which IS valid Ruby.
|
|
29
|
+
[:code, "#{tmp} = #{code}"],
|
|
30
|
+
# Capture the content of a block in a separate buffer. This means that `yield` will
|
|
31
|
+
# not output the content to the current buffer, but rather return the output.
|
|
32
|
+
[:capture, unique_name, compile(content)],
|
|
33
|
+
[:code, "end"],
|
|
34
|
+
# Output the content, without escaping it.
|
|
35
|
+
# Hanami has this ↴
|
|
36
|
+
# [:escape, escape, [:dynamic, tmp]]
|
|
37
|
+
[:escape, escape, [:dynamic, Brut::FrontEnd::Templates.name + "::HTMLSafeString.new(#{tmp})"]]
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Details explaining the change:
|
|
41
|
+
#
|
|
42
|
+
# The sexps for template are quite convoluted and highly dynamic, so it is hard
|
|
43
|
+
# to understand exactly what effect they will have. Basically, what this [:multi thing is
|
|
44
|
+
# doing is to capture the result of the block in a variable:
|
|
45
|
+
#
|
|
46
|
+
# some_var = form_for(args) do
|
|
47
|
+
#
|
|
48
|
+
# It then captures the inside of the block in a new variable:
|
|
49
|
+
#
|
|
50
|
+
# some_other_var = «whatever was inside that `do`»
|
|
51
|
+
#
|
|
52
|
+
# And follows it with an end.
|
|
53
|
+
#
|
|
54
|
+
# The first variable—some_var—now holds the return value of the helper, form_for in this case. To
|
|
55
|
+
# output this content to the actual view, it must be dereferenced, thus [ :dynamic, "some_var" ].
|
|
56
|
+
#
|
|
57
|
+
# We are going to treat the return value of the block helper as HTML safe. Thus, we'll wrap it
|
|
58
|
+
# with HTMLSafeString.new(…).
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# A temple "engine" that can be used to parse ERB and generate HTML
|
|
2
|
+
# in just the way we need.
|
|
3
|
+
class Brut::FrontEnd::Templates::ERBEngine < Temple::Engine
|
|
4
|
+
# Parse the ERB into sexps
|
|
5
|
+
use Brut::FrontEnd::Templates::ERBParser
|
|
6
|
+
|
|
7
|
+
# Handle block syntax used in a <%=
|
|
8
|
+
use Brut::FrontEnd::Templates::BlockFilter
|
|
9
|
+
|
|
10
|
+
# Trim whitespace like ERB does
|
|
11
|
+
use Temple::ERB::Trimming
|
|
12
|
+
|
|
13
|
+
# Escape strings only if they are not HTMLSafeString
|
|
14
|
+
use Brut::FrontEnd::Templates::EscapableFilter
|
|
15
|
+
# This filter actually runs the Ruby code
|
|
16
|
+
use Temple::Filters::StaticAnalyzer
|
|
17
|
+
# Flattens nested :multi expressions which I'm not sure is needed, but
|
|
18
|
+
# have cargo-culted from hanami
|
|
19
|
+
use Temple::Filters::MultiFlattener
|
|
20
|
+
# merges sequential :static, which again, not sure is needed, but
|
|
21
|
+
# have cargo-culted from hanami
|
|
22
|
+
use Temple::Filters::StaticMerger
|
|
23
|
+
|
|
24
|
+
# This generates everything into a string
|
|
25
|
+
use Temple::Generators::ArrayBuffer
|
|
26
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Almost verbatim copy of Hanami's parser:
|
|
2
|
+
#
|
|
3
|
+
# https://github.com/hanami/view/blob/main/lib/hanami/view/erb/parser.rb
|
|
4
|
+
#
|
|
5
|
+
# That is licensed MIT and thus so is this.
|
|
6
|
+
#
|
|
7
|
+
# Avoid changes to this file so it can be kept updated with Hanami.
|
|
8
|
+
class Brut::FrontEnd::Templates::ERBParser < Temple::Parser
|
|
9
|
+
ERB_PATTERN = /(\n|<%%|%%>)|<%(==?|\#)?(.*?)?-?%>/m
|
|
10
|
+
|
|
11
|
+
IF_UNLESS_CASE_LINE_RE = /\A\s*(if|unless|case)\b/
|
|
12
|
+
BLOCK_LINE_RE = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
|
|
13
|
+
END_LINE_RE = /\bend\b/
|
|
14
|
+
|
|
15
|
+
def call(input)
|
|
16
|
+
results = [[:multi]]
|
|
17
|
+
pos = 0
|
|
18
|
+
|
|
19
|
+
input.scan(ERB_PATTERN) do |token, indicator, code|
|
|
20
|
+
# Capture any text between the last ERB tag and the current one, and update the position
|
|
21
|
+
# to match the end of the current tag for the next iteration of text collection.
|
|
22
|
+
text = input[pos...$~.begin(0)]
|
|
23
|
+
pos = $~.end(0)
|
|
24
|
+
|
|
25
|
+
if token
|
|
26
|
+
# First, handle certain static tokens picked up by our ERB_PATTERN regexp. These are
|
|
27
|
+
# newlines as well as the special codes for literal `<%` and `%>` values.
|
|
28
|
+
case token
|
|
29
|
+
when "\n"
|
|
30
|
+
results.last << [:static, "#{text}\n"] << [:newline]
|
|
31
|
+
when "<%%", "%%>"
|
|
32
|
+
results.last << [:static, text] unless text.empty?
|
|
33
|
+
token.slice!(1)
|
|
34
|
+
results.last << [:static, token]
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
# Next, handle actual ERB tags. Start by adding any static text between this match and
|
|
38
|
+
# the last.
|
|
39
|
+
results.last << [:static, text] unless text.empty?
|
|
40
|
+
|
|
41
|
+
case indicator
|
|
42
|
+
when "#"
|
|
43
|
+
# Comment tags: <%# this is a comment %>
|
|
44
|
+
results.last << [:code, "\n" * code.count("\n")]
|
|
45
|
+
when %r{=}
|
|
46
|
+
# Expression tags: <%= "hello (auto-escaped)" %> or <%== "hello (not escaped)" %>
|
|
47
|
+
if code =~ BLOCK_LINE_RE
|
|
48
|
+
# See Hanami::View::Erb::Filters::Block for the processing of `:erb, :block` sexps
|
|
49
|
+
block_node = [:erb, :block, indicator.size == 1, code, (block_content = [:multi])]
|
|
50
|
+
results.last << block_node
|
|
51
|
+
|
|
52
|
+
# For blocks opened in ERB expression tags, push this `[:multi]` sexp
|
|
53
|
+
# (representing the content of the block) onto the stack of resuts. This allows
|
|
54
|
+
# subsequent results to be appropriately added inside the block, until its closing
|
|
55
|
+
# tag is encountered, and this `block_content` multi is subsequently popped off
|
|
56
|
+
# the results stack.
|
|
57
|
+
results << block_content
|
|
58
|
+
else
|
|
59
|
+
results.last << [:escape, indicator.size == 1, [:dynamic, code]]
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
# Code tags: <% if some_cond %>
|
|
63
|
+
if code =~ BLOCK_LINE_RE || code =~ IF_UNLESS_CASE_LINE_RE
|
|
64
|
+
results.last << [:code, code]
|
|
65
|
+
|
|
66
|
+
# For ERB code tags that will result in a matching `end`, push the last result
|
|
67
|
+
# back onto the stack of results. This might seem redundant, but it allows
|
|
68
|
+
# subsequent sexps to continue to be pushed onto the same result while also
|
|
69
|
+
# allowing it to be safely popped again when the matching `end` is encountered.
|
|
70
|
+
results << results.last
|
|
71
|
+
elsif code =~ END_LINE_RE
|
|
72
|
+
results.last << [:code, code]
|
|
73
|
+
results.pop
|
|
74
|
+
else
|
|
75
|
+
results.last << [:code, code]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Add any text after the final ERB tag
|
|
82
|
+
results.last << [:static, input[pos..-1]]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# A temple filter that handles escaping HTML unless it's been wrapped in
|
|
2
|
+
# an HTMLSafeString.
|
|
3
|
+
class Brut::FrontEnd::Templates::EscapableFilter < Temple::Filters::Escapable
|
|
4
|
+
using Brut::FrontEnd::Templates::HTMLSafeString::Refinement
|
|
5
|
+
|
|
6
|
+
def initialize(opts = {})
|
|
7
|
+
opts[:escape_code] ||= "::Brut::FrontEnd::Templates::EscapableFilter.escape_html((%s))"
|
|
8
|
+
super(opts)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.escape_html(html)
|
|
12
|
+
if html.kind_of?(Brut::FrontEnd::Templates::HTMLSafeString)
|
|
13
|
+
html.string
|
|
14
|
+
else
|
|
15
|
+
Temple::Utils.escape_html(html)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# A wrapper around a string to indicate it is HTML-safe and
|
|
2
|
+
# can be rendered directly without escaping.
|
|
3
|
+
class Brut::FrontEnd::Templates::HTMLSafeString
|
|
4
|
+
module Refinement
|
|
5
|
+
refine String do
|
|
6
|
+
def html_safe! = Brut::FrontEnd::Templates::HTMLSafeString.from_string(self)
|
|
7
|
+
def html_safe? = false
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
attr_reader :string
|
|
11
|
+
def initialize(string)
|
|
12
|
+
@string = string
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Wrap a string in an HTMLSafeString if needed.
|
|
16
|
+
def self.from_string(string_or_html_safe_string)
|
|
17
|
+
if string_or_html_safe_string.kind_of?(self)
|
|
18
|
+
string_or_html_safe_string
|
|
19
|
+
else
|
|
20
|
+
self.new(string_or_html_safe_string)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# This must be convertible to a string
|
|
25
|
+
def to_s = @string
|
|
26
|
+
def to_str = @string
|
|
27
|
+
def html_safe! = self
|
|
28
|
+
def html_safe? = true
|
|
29
|
+
def capitalize = self.class.new(@string.capitalize)
|
|
30
|
+
def downcase = self.class.new(@string.downcase)
|
|
31
|
+
def upcase = self.class.new(@string.upcase)
|
|
32
|
+
|
|
33
|
+
def +(other)
|
|
34
|
+
if other.html_safe?
|
|
35
|
+
self.class.new(@string + other.to_s)
|
|
36
|
+
else
|
|
37
|
+
@string + other.to_s
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|