brut 0.0.13 → 0.0.21
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 +4 -4
- data/Gemfile.lock +4 -6
- data/brut.gemspec +1 -3
- data/lib/brut/back_end/seed_data.rb +19 -2
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
- data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
- data/lib/brut/back_end/sidekiq.rb +2 -1
- data/lib/brut/back_end/validator.rb +5 -1
- data/lib/brut/back_end.rb +9 -0
- data/lib/brut/cli/apps/scaffold.rb +16 -24
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/factory_bot.rb +0 -5
- data/lib/brut/framework/app.rb +70 -5
- data/lib/brut/framework/config.rb +9 -46
- data/lib/brut/framework/container.rb +3 -2
- data/lib/brut/framework/errors.rb +12 -4
- data/lib/brut/framework/mcp.rb +63 -2
- data/lib/brut/framework/project_environment.rb +6 -2
- data/lib/brut/framework.rb +1 -1
- data/lib/brut/front_end/asset_path_resolver.rb +15 -0
- data/lib/brut/front_end/component.rb +101 -246
- data/lib/brut/front_end/components/constraint_violations.rb +10 -10
- data/lib/brut/front_end/components/form_tag.rb +17 -29
- data/lib/brut/front_end/components/i18n_translations.rb +12 -13
- data/lib/brut/front_end/components/input.rb +0 -1
- data/lib/brut/front_end/components/inputs/csrf_token.rb +3 -3
- data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
- data/lib/brut/front_end/components/inputs/select.rb +13 -20
- data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
- data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
- data/lib/brut/front_end/components/locale_detection.rb +2 -2
- data/lib/brut/front_end/components/page_identifier.rb +3 -5
- data/lib/brut/front_end/components/{time.rb → time_tag.rb} +14 -11
- data/lib/brut/front_end/components/traceparent.rb +5 -6
- data/lib/brut/front_end/http_method.rb +4 -0
- data/lib/brut/front_end/inline_svg_locator.rb +21 -0
- data/lib/brut/front_end/layout.rb +19 -0
- data/lib/brut/front_end/page.rb +52 -40
- data/lib/brut/front_end/request_context.rb +13 -0
- data/lib/brut/front_end/routing.rb +8 -3
- data/lib/brut/front_end.rb +32 -0
- data/lib/brut/i18n/base_methods.rb +51 -11
- data/lib/brut/i18n/for_back_end.rb +8 -0
- data/lib/brut/i18n/for_cli.rb +5 -1
- data/lib/brut/i18n/for_html.rb +9 -1
- data/lib/brut/i18n/http_accept_language.rb +47 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/open_telemetry.rb +25 -0
- data/lib/brut/instrumentation.rb +3 -5
- data/lib/brut/sinatra_helpers.rb +13 -7
- data/lib/brut/spec_support/component_support.rb +27 -13
- data/lib/brut/spec_support/e2e_support.rb +4 -0
- data/lib/brut/spec_support/general_support.rb +3 -0
- data/lib/brut/spec_support/handler_support.rb +6 -1
- data/lib/brut/spec_support/matcher.rb +1 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +3 -1
- data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
- data/lib/brut/spec_support/rspec_setup.rb +1 -0
- data/lib/brut/spec_support.rb +5 -4
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +7 -50
- metadata +14 -49
- data/doc-src/architecture.md +0 -102
- data/doc-src/assets.md +0 -98
- data/doc-src/forms.md +0 -214
- data/doc-src/handlers.md +0 -83
- data/doc-src/javascript.md +0 -265
- data/doc-src/keyword-injection.md +0 -183
- data/doc-src/pages.md +0 -210
- data/doc-src/route-hooks.md +0 -59
- data/lib/brut/front_end/template.rb +0 -47
- data/lib/brut/front_end/templates/block_filter.rb +0 -61
- data/lib/brut/front_end/templates/erb_engine.rb +0 -26
- data/lib/brut/front_end/templates/erb_parser.rb +0 -84
- data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
- data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
- data/lib/brut/front_end/templates/locator.rb +0 -60
data/doc-src/pages.md
DELETED
@@ -1,210 +0,0 @@
|
|
1
|
-
# Pages and Components
|
2
|
-
|
3
|
-
A website is made up of pages. Even a web *app* has pages. Thus, the most basic way to create dynamic behavior with Brut is the
|
4
|
-
*page*. In Brut, a page is a URL, a subclass of {Brut::FrontEnd::Page}, and an ERB template. The template is rendered in the context
|
5
|
-
of an instance of the class whenever the URL is requested.
|
6
|
-
|
7
|
-
## Overview of Rendering Pages
|
8
|
-
|
9
|
-
In your `app.rb`, you first declare a page using the {Brut::SinatraHelpers::ClassMethods#page} method (inside the block given to {Brut::Framework::App.routes}):
|
10
|
-
|
11
|
-
class App < Brut::Framework::App
|
12
|
-
|
13
|
-
routes do
|
14
|
-
|
15
|
-
page "/sign_in"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
The name of the route must start with a `/` and the rest of the route determines the name of the page. In the example above, Brut
|
20
|
-
will expect to find a class named `SignInPage`, which should be located in `app/src/front_end/pages`. You can create it with
|
21
|
-
`bin/scaffold`:
|
22
|
-
|
23
|
-
> bin/scaffold page SignInPage
|
24
|
-
|
25
|
-
The page class must subclass {Brut::FrontEnd::Page}. You write a constructor to accept whatever your page needs to assemble the data
|
26
|
-
needed to render it.
|
27
|
-
|
28
|
-
The HTML is generated based on an ERB file located in `app/src/front_end/pages`. In this example, that's
|
29
|
-
`app/src/front_end/pages/sign_in_page.html.erb`. That ERB is executed with an instance of the page class as context. That means you
|
30
|
-
can references any methods or ivars.
|
31
|
-
|
32
|
-
## Creating a Page Class
|
33
|
-
|
34
|
-
{Brut::FrontEnd::Page#render} is called by Brut and expects to receive HTML, which it will send as the body of the response. `render`
|
35
|
-
manages the ERB rendering, so the bulk of your page's logic will be in other methods. The constructor is where any data you need is
|
36
|
-
provided.
|
37
|
-
|
38
|
-
A page's `initialize` method must accept only keyword arguments and those keywords are used by Brut to determine what data needs to be
|
39
|
-
passed in. This technique, called *{file:doc-src/keyword-injection.md Keyword Injection}*, is used in several other places in Brut.
|
40
|
-
|
41
|
-
When the page's URL is requested, an instance of your page class is created, passing in the values needed. Beyond that, however, your page class is a normal Ruby class. You can save data to instance variables, implement attributes or other methods. Basically, whatever logic your page needs to render will exist in the page class.
|
42
|
-
|
43
|
-
## Implementing Your ERB
|
44
|
-
|
45
|
-
ERB is part of the Ruby standard library that allows the creation of dynamic templates. Brut's ERB is close to this implementation, but has a few additional features that make working with HTML a bit safer.
|
46
|
-
|
47
|
-
### Your Page is the Only Context
|
48
|
-
|
49
|
-
The instance of your page class is the only context available to the ERB. There is no set of global helpers that are injected, nor
|
50
|
-
are there any modules added when the page is rendered. Anything you need to do in your ERB must be available to the page class you've
|
51
|
-
created.
|
52
|
-
|
53
|
-
The reason for this is to keep things simple. If your page's HTML needs access to a lot of stuff, your page class will be the source
|
54
|
-
of truth as to where that stuff comes from. You will always be able to figure out where methods are defined by looking at the page
|
55
|
-
class or any of its ancestors. You are free to `include` as many modules as you like, or dump lots of methods into your `AppPage`
|
56
|
-
base class. But you don't have to.
|
57
|
-
|
58
|
-
### All Strings are HTML-Escaped
|
59
|
-
|
60
|
-
Any instance of a `String` is HTML-escaped by Brut before being inserted into the HTML being generated by the ERB. This is, generally, what you want to have happen. If you have a string that you believe is safe and does not need to be escaped, you can prevent HTML-escaping in one of two ways:
|
61
|
-
|
62
|
-
* Call {Brut::FrontEnd::Component#html_safe!} on the string
|
63
|
-
|
64
|
-
<%= html_safe!(get_value) %>
|
65
|
-
|
66
|
-
* Wrap the string in a {Brut::FrontEnd::Templates::HTMLSafeString}.
|
67
|
-
|
68
|
-
def get_value
|
69
|
-
Brut::FrontEnd::Templates::HTMLSafeString.from_string(some_value)
|
70
|
-
end
|
71
|
-
|
72
|
-
This is what `html_safe!` does and this is what Brut does internally. Brut doesn't monkey-patch `String`. A `String` is always
|
73
|
-
considered unsafe, and an `HTMLSafeString` is always considered safe.
|
74
|
-
|
75
|
-
Both of these methods are verbose, but this is on purpose. You generally don't want un-escaped HTML going into your HTML, but if you
|
76
|
-
do, you may find it better to create a component (see below), or use {Brut::FrontEnd::Component::Helpers#html_tag} to generate markup.
|
77
|
-
|
78
|
-
### Pages Have Layouts
|
79
|
-
|
80
|
-
The page's ERB is rendered in the context of a *layout*. A Layout works similar to how it works in Rails. It's intended to hold HTML
|
81
|
-
needed by every page of your app. A minimal layout might look like so:
|
82
|
-
|
83
|
-
<!DOCTYPE html>
|
84
|
-
<html>
|
85
|
-
<head>
|
86
|
-
<meta charset="utf-8">
|
87
|
-
<%= component(Brut::FrontEnd::Components::PageIdentifier.new(self.page_name)) %>
|
88
|
-
<link rel="preload" as="style" href="<%= asset_path('/css/styles.css') %>">
|
89
|
-
<link rel="stylesheet" href="<%= asset_path('/css/styles.css') %>">
|
90
|
-
<script defer src="<%= asset_path('/js/app.js') %>"></script>
|
91
|
-
</head>
|
92
|
-
<body>
|
93
|
-
<%= yield %>
|
94
|
-
</body>
|
95
|
-
</html>
|
96
|
-
|
97
|
-
The layout is rendered in the context of the page class, so every page must provide whatever features are needed by the layout. This
|
98
|
-
is usually done by adding globally-used functions to `AppPage`, which is the base class of all your app's pages.
|
99
|
-
|
100
|
-
A page can use another layout by overriding {Brut::FrontEnd::Page#layout}. The string returned must match a file in
|
101
|
-
`app/src/front_end/layouts/`.
|
102
|
-
|
103
|
-
### There Are No Partials
|
104
|
-
|
105
|
-
Rails partials are not part of ERB, and Brut does not include this feature. Instead, you would use *Components*.
|
106
|
-
|
107
|
-
## Decompose and Re-Use with Components
|
108
|
-
|
109
|
-
*Components* in Brut are very similar to the View Components library, though somewhat simpler. A component is a class and an ERB
|
110
|
-
template. That template is rendered in the context of an instance of the class. That class is created the same way a page is and has
|
111
|
-
a `render` method that works just like a page's.
|
112
|
-
|
113
|
-
This is all because {Brut::FrontEnd::Page} extends {Brut::FrontEnd::Component}. A page adds the concept of a layout, but generally a
|
114
|
-
page and a component are the same thing.
|
115
|
-
|
116
|
-
### Using Components
|
117
|
-
|
118
|
-
The main difference from your perspective is that generally *you* create instances of components. This means that your page must have
|
119
|
-
access to any data a component needs. When you've created your component instance, use {Brut::FrontEnd::Component#component} to
|
120
|
-
render it:
|
121
|
-
|
122
|
-
<%= component(Button.new(type: :danger, label: "Cancel Subscription")) %>
|
123
|
-
|
124
|
-
### Components with Templates
|
125
|
-
|
126
|
-
The most common way to use a component is with an ERB template. It is expected to be in `app/src/front_end/components`. For the
|
127
|
-
hypothetical `Button` component above, Brut would expect `app/src/front_end/components/button.html.erb` to exist as a template.
|
128
|
-
|
129
|
-
Just like a page, the component's ERB is rendered in the context of the component only.
|
130
|
-
|
131
|
-
Sometimes, components are simple enough that you don't need HTML.
|
132
|
-
|
133
|
-
### Components That Render Themselves
|
134
|
-
|
135
|
-
While overriding `render` in a page is generally discouraged, {Brut::FrontEnd::Component#render} can be overridden if you want to
|
136
|
-
generate HTML yourself. The best way to do that is with {Brut::FrontEnd::Component::Helpers#html_tag}, though you can always return a
|
137
|
-
`String` or {Brut::FrontEnd::Templates::HTMLSafeString} that you've created yourself. Just remeber that all `String` instances will be
|
138
|
-
HTML-escaped.
|
139
|
-
|
140
|
-
As an example, here is how you might have a Markdown component that renders Markdown as HTML:
|
141
|
-
|
142
|
-
class MarkdownComponent < AppComponent
|
143
|
-
def initialize(markdown:)
|
144
|
-
@markdown = markdown
|
145
|
-
@renderer = Redcarpet::Markdown.new(
|
146
|
-
Redcarpet::Render::HTML.new(
|
147
|
-
filter_html: true,
|
148
|
-
no_images: true,
|
149
|
-
no_styles: true,
|
150
|
-
safe_links_only: true,
|
151
|
-
),
|
152
|
-
fenced_code_blocks: true,
|
153
|
-
autolink: true,
|
154
|
-
quote: true,
|
155
|
-
)
|
156
|
-
end
|
157
|
-
|
158
|
-
def render
|
159
|
-
html_safe!(@renderer.render(@markdown.to_s))
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
|
164
|
-
### Global Components
|
165
|
-
|
166
|
-
While a component is just a class with an initializer, sometimes you need a component that is generally useful on any page, but that
|
167
|
-
you don't want to initialize. For example, if your component needs access to the flash, but your page does not, you don't want your
|
168
|
-
page to require a flash just to pass to the component. In that case, a *global component* can be used and Brut will instantiate it.
|
169
|
-
|
170
|
-
{Brut::FrontEnd::Component#component} can be given a class, and Brut will use keyword injection to create it. Consider a generic
|
171
|
-
flash component:
|
172
|
-
|
173
|
-
class GlobalFlash < AppComponent
|
174
|
-
|
175
|
-
attr_reader :flash
|
176
|
-
|
177
|
-
def initialize(flash:)
|
178
|
-
@flash = flash
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
You can use this anywhere like so:
|
183
|
-
|
184
|
-
<%= component(GlobalFlash) %>
|
185
|
-
|
186
|
-
Because the flash is availble from the {Brut::FrontEnd::RequestContext}, Brut can create this component when needed.
|
187
|
-
|
188
|
-
## Testing Pages and Components
|
189
|
-
|
190
|
-
Pages and Components are classes with a constructor you create and a well-defined primary method called `render`. This means you can
|
191
|
-
test them conventionally, since they can be directly created in your tests.
|
192
|
-
|
193
|
-
That said, you likely want to test the generated HTML and not the methods of the class. All tests of a page or component have the
|
194
|
-
methods in {Brut::SpecSupport::ComponentSupport} available. Of particular interest is
|
195
|
-
{Brut::SpecSupport::ComponentSupport#render_and_parse}.
|
196
|
-
|
197
|
-
This method accepts an instance of a page or component, parses the resulting HTML with Nokogiri, and returns a
|
198
|
-
{Brut::SpecSupport::EnhancedNode}, which wraps a `Nokogiri::XML::Node` or `Nokogiri::XML::Element`, depending on what was parsed. You
|
199
|
-
can then use Nokogiri's API locate elements and assert on them.
|
200
|
-
|
201
|
-
To keep close to the web platform, it's recommended to use CSS selectors either via {Brut::SpecSupport::EnhancedNode#e} or {Brut::SpecSupport::EnhancedNode#e!} (both of which wrap Nokogiri's `css` method).
|
202
|
-
|
203
|
-
Instead of creating another API for accessing HTML content, the Nokogiri API should be used directly. You can create custom matchers
|
204
|
-
as needed for common assertions. Brut includes a few that you will find useful:
|
205
|
-
|
206
|
-
* `have_link_to`
|
207
|
-
* `have_html_attribute`
|
208
|
-
* `have_i18n_string`
|
209
|
-
|
210
|
-
|
data/doc-src/route-hooks.md
DELETED
@@ -1,59 +0,0 @@
|
|
1
|
-
# Route and Page Hooks
|
2
|
-
|
3
|
-
Route and page hooks allow you to perform logic or redirect the visitor before a page is rendered or action handled.
|
4
|
-
|
5
|
-
## Route Hooks
|
6
|
-
|
7
|
-
Route hooks are objects that are run before or after a request has been handled. They are useful for setting up cross-cutting code
|
8
|
-
that you don't want to have inside a page or handler.
|
9
|
-
|
10
|
-
To use one, call either {Brut::Framework::App.before} or {Brut::Framework::App.after}, passing it the *name* of a class to use as the
|
11
|
-
hook (i.e. a `String`).
|
12
|
-
|
13
|
-
Then, implement that class, extending {Brut::FrontEnd::RouteHook}, and provide either {Brut::FrontEnd::RouteHook#before} or {Brut::FrontEnd::RouteHook#after}. As discussed in {file:doc-src/keyword-injection.md Keyword Injection}, your hook can be passed some managed values to allow it to work.
|
14
|
-
|
15
|
-
In general, a hook will allow the request to continue or not, but using one of the following methods as the return value:
|
16
|
-
|
17
|
-
* {Brut::FrontEnd::HandlingResults#redirect_to} to redirect the user instead of rendering the page or handling the request.
|
18
|
-
* {Brut::FrontEnd::HandlingResults#http_status} to return an HTTP status instead of rendering the page or handling the request.
|
19
|
-
* {Brut::FrontEnd::RouteHook#continue} to proceed with the request.
|
20
|
-
|
21
|
-
## Page Hooks
|
22
|
-
|
23
|
-
Sometimes, the behavior you want to manage before a page is rendered is specific to a page and not cross-cutting. Because a page
|
24
|
-
exepcts to render HTML, you cannot easily put such code in your page class.
|
25
|
-
|
26
|
-
If you implement {Brut::FrontEnd::Page#before_render}, you can skip page rendering entirely and redirect the user or send an error. A
|
27
|
-
good example of this would be a set of admin pages where the logged-in site visitor must possess some roles in order to see the page.
|
28
|
-
|
29
|
-
A page hook expects one of these return values:
|
30
|
-
|
31
|
-
* `URI` - redirect the visitor instead of rendering the page.
|
32
|
-
* {Brut::FrontEnd::HttpStatus} - Send the browser this status code instead of rendering the page.
|
33
|
-
* Anything else - render the page as normal
|
34
|
-
|
35
|
-
Thus, the lifecycle of a page is:
|
36
|
-
|
37
|
-
1. "Before" Route Hooks
|
38
|
-
2. Page Initializer, injected as described in {file:doc-src/keyword-injection.md}
|
39
|
-
3. Page's `before_render`, called with no arguments.
|
40
|
-
4. Page's ERB generates HTML
|
41
|
-
5. "After" Route Hooks
|
42
|
-
|
43
|
-
## Handler Hooks
|
44
|
-
|
45
|
-
Like page hooks, handler hooks are called before handling logic. Implement `before_handle`. It's arguments must be a subset of the
|
46
|
-
arguments passed to `handle`. Thus, any value needed by `before_handle` must be declared as a keyword argument to `handle` as well.
|
47
|
-
|
48
|
-
If `before_handle` returns `nil`, `handle` is then called. Otherwise, `handle` is skipped and the return value of `before_handle` is
|
49
|
-
interpreted as the return value of `handle`. See {Brut::FrontEnd::Handler#handle}.
|
50
|
-
|
51
|
-
This makes the lifecycle of a handler as such:
|
52
|
-
|
53
|
-
1. "Before" Route Hooks
|
54
|
-
2. Handler Initializer, called with no argument.
|
55
|
-
3. Handler's `handle!`, injected with arguments as described in {file:doc-src/keyword-injections.md}
|
56
|
-
1. `handle!` calls `before_handle`, passing the arguments in.
|
57
|
-
2. `handle!` calls `handle`, passing the arguments in.
|
58
|
-
4. "After" Route Hooks
|
59
|
-
|
@@ -1,47 +0,0 @@
|
|
1
|
-
require "temple"
|
2
|
-
|
3
|
-
# Holds code related to rendering ERB templates
|
4
|
-
module Brut::FrontEnd::Templates
|
5
|
-
autoload(:HTMLSafeString,"brut/front_end/templates/html_safe_string")
|
6
|
-
autoload(:ERBParser,"brut/front_end/templates/erb_parser")
|
7
|
-
autoload(:EscapableFilter,"brut/front_end/templates/escapable_filter")
|
8
|
-
autoload(:BlockFilter,"brut/front_end/templates/block_filter")
|
9
|
-
autoload(:ERBEngine,"brut/front_end/templates/erb_engine")
|
10
|
-
autoload(:Locator,"brut/front_end/templates/locator")
|
11
|
-
end
|
12
|
-
|
13
|
-
# Handles rendering HTML templates written in ERB. This is a light wrapper around `Tilt`.
|
14
|
-
# This also configured a few customizations to allow a Rails-like rendering of ERB:
|
15
|
-
#
|
16
|
-
# * HTML escaping by default
|
17
|
-
# * Helpers that return {Brut::FrontEnd::Templates::HTMLSafeString}s won't be escaped
|
18
|
-
#
|
19
|
-
# @see https://github.com/rtomayko/tilt
|
20
|
-
class Brut::FrontEnd::Template
|
21
|
-
|
22
|
-
# @!visibility private
|
23
|
-
# This sets up global state somewhere, even though we aren't using `TempleTemplate`
|
24
|
-
# anywhere.
|
25
|
-
TempleTemplate = Temple::Templates::Tilt(Brut::FrontEnd::Templates::ERBEngine,
|
26
|
-
register_as: "html.erb")
|
27
|
-
|
28
|
-
attr_reader :template_file_path
|
29
|
-
|
30
|
-
# Wraps a string that is deemed safe to insert into
|
31
|
-
# HTML without escaping it. This allows stuff like
|
32
|
-
# <%= component(SomeComponent) %> to work without
|
33
|
-
# having to remember to <%== all the time.
|
34
|
-
def initialize(template_file_path)
|
35
|
-
@template_file_path = template_file_path
|
36
|
-
@tilt_template = Tilt.new(@template_file_path)
|
37
|
-
end
|
38
|
-
|
39
|
-
def render_template(...)
|
40
|
-
@tilt_template.render(...)
|
41
|
-
end
|
42
|
-
|
43
|
-
# Convienience method to escape HTML in the canonical way.
|
44
|
-
def self.escape_html(string)
|
45
|
-
Brut::FrontEnd::Templates::EscapableFilter.escape_html(string)
|
46
|
-
end
|
47
|
-
end
|
@@ -1,61 +0,0 @@
|
|
1
|
-
# Allows rendering blocks in ERB the way Rails' helpers like `form_with` do.
|
2
|
-
# This is a slightly modified copy of Hanami's `Filters::Block`.
|
3
|
-
#
|
4
|
-
# @see https://github.com/hanami/view/blob/main/lib/hanami/view/erb/filters/block.rb
|
5
|
-
class Brut::FrontEnd::Templates::BlockFilter < Temple::Filter
|
6
|
-
END_LINE_RE = /\bend\b/
|
7
|
-
|
8
|
-
# @!visibility private
|
9
|
-
def on_erb_block(escape, code, content)
|
10
|
-
tmp = unique_name
|
11
|
-
|
12
|
-
# Remove the last `end` :code sexp, since this is technically "outside" the block
|
13
|
-
# contents, which we want to capture separately below. This `end` is added back after
|
14
|
-
# capturing the content below.
|
15
|
-
case content.last
|
16
|
-
in [:code, c] if c =~ END_LINE_RE
|
17
|
-
content.pop
|
18
|
-
end
|
19
|
-
|
20
|
-
[:multi,
|
21
|
-
# Capture the result of the code in a variable. We can't do `[:dynamic, code]` because
|
22
|
-
# it's probably not a complete expression (which is a requirement for Temple).
|
23
|
-
# DBC: an example is that 'code' might be "form_for do" which is not an expression.
|
24
|
-
# Because we later put an "end" in, the result will be
|
25
|
-
#
|
26
|
-
# some_var = helper do
|
27
|
-
# end
|
28
|
-
#
|
29
|
-
# Which IS valid Ruby.
|
30
|
-
[:code, "#{tmp} = #{code}"],
|
31
|
-
# Capture the content of a block in a separate buffer. This means that `yield` will
|
32
|
-
# not output the content to the current buffer, but rather return the output.
|
33
|
-
[:capture, unique_name, compile(content)],
|
34
|
-
[:code, "end"],
|
35
|
-
# Output the content, without escaping it.
|
36
|
-
# Hanami has this ↴
|
37
|
-
# [:escape, escape, [:dynamic, tmp]]
|
38
|
-
[:escape, escape, [:dynamic, Brut::FrontEnd::Templates.name + "::HTMLSafeString.new(#{tmp})"]]
|
39
|
-
]
|
40
|
-
|
41
|
-
# Details explaining the change:
|
42
|
-
#
|
43
|
-
# The sexps for template are quite convoluted and highly dynamic, so it is hard
|
44
|
-
# to understand exactly what effect they will have. Basically, what this [:multi thing is
|
45
|
-
# doing is to capture the result of the block in a variable:
|
46
|
-
#
|
47
|
-
# some_var = form_for(args) do
|
48
|
-
#
|
49
|
-
# It then captures the inside of the block in a new variable:
|
50
|
-
#
|
51
|
-
# some_other_var = «whatever was inside that `do`»
|
52
|
-
#
|
53
|
-
# And follows it with an end.
|
54
|
-
#
|
55
|
-
# The first variable—some_var—now holds the return value of the helper, form_for in this case. To
|
56
|
-
# output this content to the actual view, it must be dereferenced, thus [ :dynamic, "some_var" ].
|
57
|
-
#
|
58
|
-
# We are going to treat the return value of the block helper as HTML safe. Thus, we'll wrap it
|
59
|
-
# with HTMLSafeString.new(…).
|
60
|
-
end
|
61
|
-
end
|
@@ -1,26 +0,0 @@
|
|
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
|
@@ -1,84 +0,0 @@
|
|
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 file.
|
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
|
@@ -1,20 +0,0 @@
|
|
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
|
-
# @!visibility private
|
7
|
-
def initialize(opts = {})
|
8
|
-
opts[:escape_code] ||= "::Brut::FrontEnd::Templates::EscapableFilter.escape_html((%s))"
|
9
|
-
super(opts)
|
10
|
-
end
|
11
|
-
|
12
|
-
# @!visibility private
|
13
|
-
def self.escape_html(html)
|
14
|
-
if html.kind_of?(Brut::FrontEnd::Templates::HTMLSafeString)
|
15
|
-
html.string
|
16
|
-
else
|
17
|
-
Temple::Utils.escape_html(html)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
# A wrapper around a string to indicate it is HTML-safe and
|
2
|
-
# can be rendered directly without escaping. This was done to avoid adding methods on `String` and the internal state
|
3
|
-
# required to make something like `"foo".html_safe!` work.
|
4
|
-
class Brut::FrontEnd::Templates::HTMLSafeString
|
5
|
-
# This can be used via `using` to add `html_safe!` and `html_safe?` method to `String` when they might be more convienient
|
6
|
-
# than using {Brut::FrontEnd::Templates::HTMLSafeString} directly.
|
7
|
-
module Refinement
|
8
|
-
refine String do
|
9
|
-
def html_safe! = Brut::FrontEnd::Templates::HTMLSafeString.from_string(self)
|
10
|
-
def html_safe? = false
|
11
|
-
end
|
12
|
-
end
|
13
|
-
using Refinement
|
14
|
-
|
15
|
-
# @return [String] the underlying string being wrapped
|
16
|
-
attr_reader :string
|
17
|
-
|
18
|
-
# Create an HTML safe string based on the parameter. It's recommended to use {.from_string} instead.
|
19
|
-
#
|
20
|
-
# @param [String] string A string that is considered safe to put directly into a web page without escaping.
|
21
|
-
def initialize(string)
|
22
|
-
@string = string
|
23
|
-
end
|
24
|
-
|
25
|
-
# Creates an HTML Safe string based on the parameter, properly handling if a HTML safe string is being passed.
|
26
|
-
#
|
27
|
-
# @param [String|Brut::FrontEnd::Templates::HTMLSafeString] string_or_html_safe_string the value to turn into an HTML safe string.
|
28
|
-
#
|
29
|
-
# @return [Brut::FrontEnd::Templates::HTMLSafeString] if `string_or_html_safe_string` is already HTML safe, returns it. Otherwise,
|
30
|
-
# wraps the string as HTML safe.
|
31
|
-
def self.from_string(string_or_html_safe_string)
|
32
|
-
if string_or_html_safe_string.kind_of?(self)
|
33
|
-
string_or_html_safe_string
|
34
|
-
else
|
35
|
-
self.new(string_or_html_safe_string)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# This must be convertible to a string
|
40
|
-
def to_s = @string
|
41
|
-
def to_str = @string
|
42
|
-
# Matches the protocol in {Brut::FrontEnd::Templates::HTMLSafeString::Refinement}
|
43
|
-
# @return [Brut::FrontEnd::Templates::HTMLSafeString] self
|
44
|
-
def html_safe! = self
|
45
|
-
# Matches the protocol in {Brut::FrontEnd::Templates::HTMLSafeString::Refinement}
|
46
|
-
# @return [true|false] true
|
47
|
-
def html_safe? = true
|
48
|
-
|
49
|
-
# Return a new instance that has called `capitalize` on the underlying string
|
50
|
-
def capitalize = self.class.new(@string.capitalize)
|
51
|
-
# Return a new instance that has called `downcase` on the underlying string
|
52
|
-
def downcase = self.class.new(@string.downcase)
|
53
|
-
# Return a new instance that has called `upcase` on the underlying string
|
54
|
-
def upcase = self.class.new(@string.upcase)
|
55
|
-
|
56
|
-
# Returns the concatenation of two strings. If the other is HTML safe, then this returns an HTML safe string.
|
57
|
-
# If the other is not, this returns a normal unsafe string.
|
58
|
-
#
|
59
|
-
# @param [String|Brut::FrontEnd::Templates::HTMLSafeString] other
|
60
|
-
# @return [String|Brut::FrontEnd::Templates::HTMLSafeString] A safe or unsafe string, depending on what was passed.
|
61
|
-
def +(other)
|
62
|
-
if other.html_safe?
|
63
|
-
self.class.new(@string + other.to_s)
|
64
|
-
else
|
65
|
-
@string + other.to_s
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
@@ -1,60 +0,0 @@
|
|
1
|
-
# Locates a template, based on a name, configured paths, and an extension. This class forms both an API
|
2
|
-
# for template location ({#locate}) as well as an implementation that is conventional with Brut apps.
|
3
|
-
class Brut::FrontEnd::Templates::Locator
|
4
|
-
# Create a locator that will search the given paths and require that template
|
5
|
-
# files have the given extension
|
6
|
-
#
|
7
|
-
# @param [Pathname|String|Array<Pathname|String>] paths one or more paths that will be searched for templates
|
8
|
-
# @param [String] extension file extension, without the dot, of the name of files that are considered templates
|
9
|
-
def initialize(paths:, extension:)
|
10
|
-
@paths = Array(paths).map { |path| Pathname(path) }
|
11
|
-
@extension = extension
|
12
|
-
end
|
13
|
-
|
14
|
-
# Given a base name, which may or may not be nested paths, returns the path to the template
|
15
|
-
# for this file. There must be exactly one template that matches.
|
16
|
-
#
|
17
|
-
# @example
|
18
|
-
#
|
19
|
-
# locator = Locator.new(
|
20
|
-
# paths: [
|
21
|
-
# Brut.container.app_src_dir / "front_end" / "components",
|
22
|
-
# Brut.container.app_src_dir / "front_end" / "other_components",
|
23
|
-
# ],
|
24
|
-
# extension: "html.erb"
|
25
|
-
# )
|
26
|
-
#
|
27
|
-
# # Suppose app/src/front_end/components/foo.html.erb exists
|
28
|
-
# path = locator.locate("foo")
|
29
|
-
# # => "app/src/front_end/components/foo.html.erb"
|
30
|
-
#
|
31
|
-
# # Suppose app/src/front_end/components/bar/blah.html.erb exists
|
32
|
-
# path = locator.locate("bar/blah")
|
33
|
-
# # => "app/src/front_end/components/bar/blah.html.erb"
|
34
|
-
#
|
35
|
-
# # Suppose both app/src/front_end/components/bar/blah.html.erb and
|
36
|
-
# # app/src/front_end/other_components/bar/blah.html.erb
|
37
|
-
# # both exist
|
38
|
-
# path = locator.locate("bar/blah")
|
39
|
-
# # => raises an error since there are two matches
|
40
|
-
#
|
41
|
-
# @param [String] base_name the base name of a file that is expected to have a template. This is searched relative to the paths
|
42
|
-
# provided to the constructor, so it may have nested paths
|
43
|
-
# @return [String] path to the template for the given `base_name`
|
44
|
-
# @raise StandardError if zero or more than one templates are found
|
45
|
-
def locate(base_name)
|
46
|
-
paths_to_try = @paths.map { |path|
|
47
|
-
path / "#{base_name}.#{@extension}"
|
48
|
-
}
|
49
|
-
paths_found = paths_to_try.select { |path|
|
50
|
-
path.exist?
|
51
|
-
}
|
52
|
-
if paths_found.empty?
|
53
|
-
raise "Could not locate template for #{base_name}. Tried: #{paths_to_try.map(&:to_s).join(', ')}"
|
54
|
-
end
|
55
|
-
if paths_found.length > 1
|
56
|
-
raise "Found more than one valid pat for #{base_name}. You must rename your files to disambiguate them. These paths were all found: #{paths_found.map(&:to_s).join(', ')}"
|
57
|
-
end
|
58
|
-
return paths_found[0]
|
59
|
-
end
|
60
|
-
end
|