piko-lite-box 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/hanami-3.0.1/CHANGELOG.md +1701 -0
- data/hanami-3.0.1/LICENSE +20 -0
- data/hanami-3.0.1/README.md +89 -0
- data/hanami-3.0.1/hanami.gemspec +49 -0
- data/hanami-3.0.1/lib/hanami/app.rb +197 -0
- data/hanami-3.0.1/lib/hanami/boot.rb +5 -0
- data/hanami-3.0.1/lib/hanami/config/actions/content_security_policy.rb +140 -0
- data/hanami-3.0.1/lib/hanami/config/actions/cookies.rb +57 -0
- data/hanami-3.0.1/lib/hanami/config/actions/sessions.rb +83 -0
- data/hanami-3.0.1/lib/hanami/config/actions.rb +189 -0
- data/hanami-3.0.1/lib/hanami/config/assets.rb +84 -0
- data/hanami-3.0.1/lib/hanami/config/console.rb +79 -0
- data/hanami-3.0.1/lib/hanami/config/db.rb +35 -0
- data/hanami-3.0.1/lib/hanami/config/i18n.rb +138 -0
- data/hanami-3.0.1/lib/hanami/config/logger.rb +207 -0
- data/hanami-3.0.1/lib/hanami/config/null_config.rb +17 -0
- data/hanami-3.0.1/lib/hanami/config/router.rb +48 -0
- data/hanami-3.0.1/lib/hanami/config/views.rb +103 -0
- data/hanami-3.0.1/lib/hanami/config.rb +564 -0
- data/hanami-3.0.1/lib/hanami/constants.rb +62 -0
- data/hanami-3.0.1/lib/hanami/env.rb +52 -0
- data/hanami-3.0.1/lib/hanami/errors.rb +41 -0
- data/hanami-3.0.1/lib/hanami/extensions/action/i18n_helper.rb +64 -0
- data/hanami-3.0.1/lib/hanami/extensions/action/name_inferrer.rb +34 -0
- data/hanami-3.0.1/lib/hanami/extensions/action/slice_configured_action.rb +190 -0
- data/hanami-3.0.1/lib/hanami/extensions/action.rb +147 -0
- data/hanami-3.0.1/lib/hanami/extensions/db/repo.rb +108 -0
- data/hanami-3.0.1/lib/hanami/extensions/mailer/slice_configured_mailer.rb +134 -0
- data/hanami-3.0.1/lib/hanami/extensions/mailer.rb +28 -0
- data/hanami-3.0.1/lib/hanami/extensions/operation/slice_configured_db_operation.rb +90 -0
- data/hanami-3.0.1/lib/hanami/extensions/operation.rb +33 -0
- data/hanami-3.0.1/lib/hanami/extensions/router/errors.rb +58 -0
- data/hanami-3.0.1/lib/hanami/extensions/view/context.rb +298 -0
- data/hanami-3.0.1/lib/hanami/extensions/view/part.rb +92 -0
- data/hanami-3.0.1/lib/hanami/extensions/view/scope.rb +36 -0
- data/hanami-3.0.1/lib/hanami/extensions/view/slice_configured_context.rb +73 -0
- data/hanami-3.0.1/lib/hanami/extensions/view/slice_configured_helpers.rb +55 -0
- data/hanami-3.0.1/lib/hanami/extensions/view/slice_configured_part.rb +73 -0
- data/hanami-3.0.1/lib/hanami/extensions/view/slice_configured_view.rb +243 -0
- data/hanami-3.0.1/lib/hanami/extensions/view/standard_helpers.rb +26 -0
- data/hanami-3.0.1/lib/hanami/extensions/view.rb +38 -0
- data/hanami-3.0.1/lib/hanami/extensions.rb +30 -0
- data/hanami-3.0.1/lib/hanami/helpers/assets_helper.rb +804 -0
- data/hanami-3.0.1/lib/hanami/helpers/form_helper/form_builder.rb +1389 -0
- data/hanami-3.0.1/lib/hanami/helpers/form_helper/values.rb +75 -0
- data/hanami-3.0.1/lib/hanami/helpers/form_helper.rb +213 -0
- data/hanami-3.0.1/lib/hanami/helpers/i18n_helper.rb +241 -0
- data/hanami-3.0.1/lib/hanami/logger/rack_formatter.rb +73 -0
- data/hanami-3.0.1/lib/hanami/logger/sql_formatter.rb +80 -0
- data/hanami-3.0.1/lib/hanami/logger/sql_logger.rb +48 -0
- data/hanami-3.0.1/lib/hanami/middleware/assets.rb +21 -0
- data/hanami-3.0.1/lib/hanami/middleware/content_security_policy_nonce.rb +53 -0
- data/hanami-3.0.1/lib/hanami/middleware/public_errors_app.rb +75 -0
- data/hanami-3.0.1/lib/hanami/middleware/render_errors.rb +90 -0
- data/hanami-3.0.1/lib/hanami/port.rb +45 -0
- data/hanami-3.0.1/lib/hanami/prepare.rb +5 -0
- data/hanami-3.0.1/lib/hanami/provider/source.rb +16 -0
- data/hanami-3.0.1/lib/hanami/provider_registrar.rb +28 -0
- data/hanami-3.0.1/lib/hanami/providers/assets.rb +28 -0
- data/hanami-3.0.1/lib/hanami/providers/db/adapter.rb +75 -0
- data/hanami-3.0.1/lib/hanami/providers/db/adapters.rb +66 -0
- data/hanami-3.0.1/lib/hanami/providers/db/config.rb +44 -0
- data/hanami-3.0.1/lib/hanami/providers/db/gateway.rb +87 -0
- data/hanami-3.0.1/lib/hanami/providers/db/sql_adapter.rb +100 -0
- data/hanami-3.0.1/lib/hanami/providers/db.rb +325 -0
- data/hanami-3.0.1/lib/hanami/providers/db_logging.rb +19 -0
- data/hanami-3.0.1/lib/hanami/providers/i18n/backend.rb +373 -0
- data/hanami-3.0.1/lib/hanami/providers/i18n/locale/en.yml +57 -0
- data/hanami-3.0.1/lib/hanami/providers/i18n.rb +114 -0
- data/hanami-3.0.1/lib/hanami/providers/inflector.rb +17 -0
- data/hanami-3.0.1/lib/hanami/providers/logger.rb +41 -0
- data/hanami-3.0.1/lib/hanami/providers/mailers.rb +101 -0
- data/hanami-3.0.1/lib/hanami/providers/rack.rb +47 -0
- data/hanami-3.0.1/lib/hanami/providers/relations.rb +31 -0
- data/hanami-3.0.1/lib/hanami/providers/routes.rb +29 -0
- data/hanami-3.0.1/lib/hanami/rake_tasks.rb +54 -0
- data/hanami-3.0.1/lib/hanami/routes.rb +146 -0
- data/hanami-3.0.1/lib/hanami/settings/composite_store.rb +53 -0
- data/hanami-3.0.1/lib/hanami/settings/env_store.rb +32 -0
- data/hanami-3.0.1/lib/hanami/settings.rb +234 -0
- data/hanami-3.0.1/lib/hanami/setup.rb +6 -0
- data/hanami-3.0.1/lib/hanami/slice/router.rb +280 -0
- data/hanami-3.0.1/lib/hanami/slice/routes_helper.rb +37 -0
- data/hanami-3.0.1/lib/hanami/slice/routing/middleware/stack.rb +216 -0
- data/hanami-3.0.1/lib/hanami/slice/routing/resolver.rb +88 -0
- data/hanami-3.0.1/lib/hanami/slice/view_name_inferrer.rb +63 -0
- data/hanami-3.0.1/lib/hanami/slice.rb +1239 -0
- data/hanami-3.0.1/lib/hanami/slice_configurable.rb +72 -0
- data/hanami-3.0.1/lib/hanami/slice_name.rb +111 -0
- data/hanami-3.0.1/lib/hanami/slice_registrar.rb +215 -0
- data/hanami-3.0.1/lib/hanami/universal_logger.rb +250 -0
- data/hanami-3.0.1/lib/hanami/version.rb +45 -0
- data/hanami-3.0.1/lib/hanami/web/rack_logger.rb +126 -0
- data/hanami-3.0.1/lib/hanami/web/welcome.html.erb +584 -0
- data/hanami-3.0.1/lib/hanami/web/welcome.rb +46 -0
- data/hanami-3.0.1/lib/hanami.rb +276 -0
- data/piko-lite-box.gemspec +11 -0
- metadata +137 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
module Helpers
|
|
5
|
+
module FormHelper
|
|
6
|
+
# Values from params and form helpers.
|
|
7
|
+
#
|
|
8
|
+
# It's responsible to populate input values with data coming from params
|
|
9
|
+
# and inline values specified via form helpers like `text_field`.
|
|
10
|
+
#
|
|
11
|
+
# @since 2.1.0
|
|
12
|
+
# @api private
|
|
13
|
+
class Values
|
|
14
|
+
# @since 2.1.0
|
|
15
|
+
# @api private
|
|
16
|
+
GET_SEPARATOR = "."
|
|
17
|
+
|
|
18
|
+
# @api private
|
|
19
|
+
# @since 2.1.0
|
|
20
|
+
attr_reader :csrf_token
|
|
21
|
+
|
|
22
|
+
# @since 2.1.0
|
|
23
|
+
# @api private
|
|
24
|
+
def initialize(values: {}, params: {}, csrf_token: nil)
|
|
25
|
+
@values = values.to_h
|
|
26
|
+
@params = params.to_h
|
|
27
|
+
@csrf_token = csrf_token
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the value (if present) for the given key.
|
|
31
|
+
# Nested values are expressed with an array if symbols.
|
|
32
|
+
#
|
|
33
|
+
# @since 2.1.0
|
|
34
|
+
# @api private
|
|
35
|
+
def get(*keys)
|
|
36
|
+
get_from_params(*keys) || get_from_values(*keys)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# @since 2.1.0
|
|
42
|
+
# @api private
|
|
43
|
+
def get_from_params(*keys)
|
|
44
|
+
keys.map! { |key| /\A\d+\z/.match?(key.to_s) ? key.to_s.to_i : key }
|
|
45
|
+
@params.dig(*keys)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @since 2.1.0
|
|
49
|
+
# @api private
|
|
50
|
+
def get_from_values(*keys)
|
|
51
|
+
head, *tail = *keys
|
|
52
|
+
result = @values[head]
|
|
53
|
+
|
|
54
|
+
tail.each do |k|
|
|
55
|
+
break if result.nil?
|
|
56
|
+
|
|
57
|
+
result = dig(result, k)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @since 2.1.0
|
|
64
|
+
# @api private
|
|
65
|
+
def dig(base, key)
|
|
66
|
+
case base
|
|
67
|
+
when ::Hash then base[key]
|
|
68
|
+
when Array then base[key.to_s.to_i]
|
|
69
|
+
when ->(r) { r.respond_to?(key) } then base.public_send(key)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hanami/view"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
module Helpers
|
|
7
|
+
# Helper methods for generating HTML forms.
|
|
8
|
+
#
|
|
9
|
+
# These helpers will be automatically available in your view templates, part classes and scope
|
|
10
|
+
# classes.
|
|
11
|
+
#
|
|
12
|
+
# This module provides one primary method: {#form_for}, yielding an HTML form builder. This
|
|
13
|
+
# integrates with request params and template locals to populate the form with appropriate
|
|
14
|
+
# values.
|
|
15
|
+
#
|
|
16
|
+
# @api public
|
|
17
|
+
# @since 2.1.0
|
|
18
|
+
module FormHelper
|
|
19
|
+
require_relative "form_helper/form_builder"
|
|
20
|
+
|
|
21
|
+
# Default HTTP method for form
|
|
22
|
+
#
|
|
23
|
+
# @since 2.1.0
|
|
24
|
+
# @api private
|
|
25
|
+
DEFAULT_METHOD = "POST"
|
|
26
|
+
|
|
27
|
+
# Default charset
|
|
28
|
+
#
|
|
29
|
+
# @since 2.1.0
|
|
30
|
+
# @api private
|
|
31
|
+
DEFAULT_CHARSET = "utf-8"
|
|
32
|
+
|
|
33
|
+
# CSRF Token session key
|
|
34
|
+
#
|
|
35
|
+
# This name of this key is shared with the hanami and hanami-action gems.
|
|
36
|
+
#
|
|
37
|
+
# @since 2.1.0
|
|
38
|
+
# @api private
|
|
39
|
+
CSRF_TOKEN = :_csrf_token
|
|
40
|
+
|
|
41
|
+
include Hanami::View::Helpers::TagHelper
|
|
42
|
+
|
|
43
|
+
# Yields a form builder for constructing an HTML form and returns the resulting form string.
|
|
44
|
+
#
|
|
45
|
+
# See {FormHelper::FormBuilder} for the methods for building the form's fields.
|
|
46
|
+
#
|
|
47
|
+
# @overload form_for(base_name, url, values: _form_for_values, params: _form_for_params, **attributes)
|
|
48
|
+
# Builds the form using the given base name for all fields.
|
|
49
|
+
#
|
|
50
|
+
# @param base_name [String] the base
|
|
51
|
+
# @param url [String] the URL for submitting the form
|
|
52
|
+
# @param values [Hash] values to be used for populating form field values; optional,
|
|
53
|
+
# defaults to the template's locals or to a part's `{name => self}`
|
|
54
|
+
# @param params [Hash] request param values to be used for populating form field values;
|
|
55
|
+
# these are used in preference over the `values`; optional, defaults to the current
|
|
56
|
+
# request's params
|
|
57
|
+
# @param attributes [Hash] the HTML attributes for the form tag
|
|
58
|
+
# @yieldparam [FormHelper::FormBuilder] f the form builder
|
|
59
|
+
#
|
|
60
|
+
# @overload form_for(url, values: _form_for_values, params: _form_for_params, **attributes)
|
|
61
|
+
# @param url [String] the URL for submitting the form
|
|
62
|
+
# @param values [Hash] values to be used for populating form field values; optional,
|
|
63
|
+
# defaults to the template's locals or to a part's `{name => self}`
|
|
64
|
+
# @param params [Hash] request param values to be used for populating form field values;
|
|
65
|
+
# these are used in preference over the `values`; optional, defaults to the current
|
|
66
|
+
# request's params
|
|
67
|
+
# @param attributes [Hash] the HTML attributes for the form tag
|
|
68
|
+
# @yieldparam [FormHelper::FormBuilder] f the form builder
|
|
69
|
+
#
|
|
70
|
+
# @return [String] the form HTML
|
|
71
|
+
#
|
|
72
|
+
# @see FormHelper
|
|
73
|
+
# @see FormHelper::FormBuilder
|
|
74
|
+
#
|
|
75
|
+
# @example Basic usage
|
|
76
|
+
# <%= form_for("book", "/books", class: "form-horizontal") do |f| %>
|
|
77
|
+
# <div>
|
|
78
|
+
# <%= f.label "title" %>
|
|
79
|
+
# <%= f.text_field "title", class: "form-control" %>
|
|
80
|
+
# </div>
|
|
81
|
+
#
|
|
82
|
+
# <%= f.submit "Create" %>
|
|
83
|
+
# <% end %>
|
|
84
|
+
#
|
|
85
|
+
# =>
|
|
86
|
+
# <form action="/books" method="POST" accept-charset="utf-8" class="form-horizontal">
|
|
87
|
+
# <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
|
|
88
|
+
# <div>
|
|
89
|
+
# <label for="book-title">Title</label>
|
|
90
|
+
# <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
|
|
91
|
+
# </div>
|
|
92
|
+
#
|
|
93
|
+
# <button type="submit">Create</button>
|
|
94
|
+
# </form>
|
|
95
|
+
#
|
|
96
|
+
# @example Without base name
|
|
97
|
+
#
|
|
98
|
+
# <%= form_for("/books", class: "form-horizontal") do |f| %>
|
|
99
|
+
# <div>
|
|
100
|
+
# <%= f.label "books.title" %>
|
|
101
|
+
# <%= f.text_field "books.title", class: "form-control" %>
|
|
102
|
+
# </div>
|
|
103
|
+
#
|
|
104
|
+
# <%= f.submit "Create" %>
|
|
105
|
+
# <% end %>
|
|
106
|
+
#
|
|
107
|
+
# =>
|
|
108
|
+
# <form action="/books" method="POST" accept-charset="utf-8" class="form-horizontal">
|
|
109
|
+
# <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
|
|
110
|
+
# <div>
|
|
111
|
+
# <label for="book-title">Title</label>
|
|
112
|
+
# <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
|
|
113
|
+
# </div>
|
|
114
|
+
#
|
|
115
|
+
# <button type="submit">Create</button>
|
|
116
|
+
# </form>
|
|
117
|
+
#
|
|
118
|
+
# @example Method override
|
|
119
|
+
# <%= form_for("/books/123", method: :put) do |f|
|
|
120
|
+
# <%= f.text_field "book.title" %>
|
|
121
|
+
# <%= f.submit "Update" %>
|
|
122
|
+
# <% end %>
|
|
123
|
+
#
|
|
124
|
+
# =>
|
|
125
|
+
# <form action="/books/123" accept-charset="utf-8" method="POST">
|
|
126
|
+
# <input type="hidden" name="_method" value="PUT">
|
|
127
|
+
# <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
|
|
128
|
+
# <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
|
|
129
|
+
#
|
|
130
|
+
# <button type="submit">Update</button>
|
|
131
|
+
# </form>
|
|
132
|
+
#
|
|
133
|
+
# @example Overriding values
|
|
134
|
+
# <%= form_for("/songs", values: {song: {title: "Envision"}}) do |f| %>
|
|
135
|
+
# <%= f.text_field "song.title" %>
|
|
136
|
+
# <%= f.submit "Create" %>
|
|
137
|
+
# <%= end %>
|
|
138
|
+
#
|
|
139
|
+
# =>
|
|
140
|
+
# <form action="/songs" accept-charset="utf-8" method="POST">
|
|
141
|
+
# <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
|
|
142
|
+
# <input type="text" name="song[title]" id="song-title" value="Envision">
|
|
143
|
+
#
|
|
144
|
+
# <button type="submit">Create</button>
|
|
145
|
+
# </form>
|
|
146
|
+
#
|
|
147
|
+
# @api public
|
|
148
|
+
# @since 2.1.0
|
|
149
|
+
def form_for(base_name, url = nil, values: _form_for_values, params: _form_for_params, **attributes)
|
|
150
|
+
url, base_name = base_name, nil if url.nil?
|
|
151
|
+
|
|
152
|
+
values = Values.new(values: values, params: params, csrf_token: _form_csrf_token)
|
|
153
|
+
|
|
154
|
+
builder = FormBuilder.new(
|
|
155
|
+
base_name: base_name,
|
|
156
|
+
values: values,
|
|
157
|
+
inflector: _context.inflector,
|
|
158
|
+
form_attributes: attributes
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
content = (block_given? ? yield(builder) : "").html_safe
|
|
162
|
+
|
|
163
|
+
builder.call(content, action: url, **attributes)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Returns CSRF meta tags for use via unobtrusive JavaScript (UJS) libraries.
|
|
167
|
+
#
|
|
168
|
+
# @return [String, nil] the tags, if a CSRF token is available, or nil
|
|
169
|
+
#
|
|
170
|
+
# @example
|
|
171
|
+
# csrf_meta_tags
|
|
172
|
+
#
|
|
173
|
+
# =>
|
|
174
|
+
# <meta name="csrf-param" content="_csrf_token">
|
|
175
|
+
# <meta name="csrf-token" content="4a038be85b7603c406dcbfad4b9cdf91ec6ca138ed6441163a07bb0fdfbe25b5">
|
|
176
|
+
#
|
|
177
|
+
# @api public
|
|
178
|
+
# @since 2.1.0
|
|
179
|
+
def csrf_meta_tags
|
|
180
|
+
return unless (token = _form_csrf_token)
|
|
181
|
+
|
|
182
|
+
tag.meta(name: "csrf-param", content: CSRF_TOKEN) +
|
|
183
|
+
tag.meta(name: "csrf-token", content: token)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @api private
|
|
187
|
+
# @since 2.1.0
|
|
188
|
+
def _form_for_values
|
|
189
|
+
if respond_to?(:_locals) # Scope
|
|
190
|
+
_locals
|
|
191
|
+
elsif respond_to?(:_name) # Part
|
|
192
|
+
{_name => self}
|
|
193
|
+
else
|
|
194
|
+
{}
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @api private
|
|
199
|
+
# @since 2.1.0
|
|
200
|
+
def _form_for_params
|
|
201
|
+
_context.request.params
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @since 2.1.0
|
|
205
|
+
# @api private
|
|
206
|
+
def _form_csrf_token
|
|
207
|
+
return unless _context.request.session_enabled?
|
|
208
|
+
|
|
209
|
+
_context.csrf_token
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/escape"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
module Helpers
|
|
7
|
+
# View-layer translation and localization helpers.
|
|
8
|
+
#
|
|
9
|
+
# These helpers are automatically available in your view templates, part classes, and scope
|
|
10
|
+
# classes when the `i18n` gem is bundled. They provide `translate`/`t`, `translate!`/`t!`, and
|
|
11
|
+
# `localize`/`l`, sourcing the i18n backend from the view context and expanding relative
|
|
12
|
+
# (leading-dot) translation keys against the currently-rendering template name.
|
|
13
|
+
#
|
|
14
|
+
# The shared `translate`/`localize` logic lives in {Methods}, which is also included by the
|
|
15
|
+
# action-layer i18n helper ({Hanami::Extensions::Action::I18nHelper}). This module supplies
|
|
16
|
+
# the two view-specific concrete implementations of {Methods}'s abstract hooks: {#_i18n}
|
|
17
|
+
# returns the i18n backend from the view context, and {#_resolve_i18n_key} expands relative
|
|
18
|
+
# keys against the template name.
|
|
19
|
+
#
|
|
20
|
+
# @example Basic translation
|
|
21
|
+
# <%= translate("messages.welcome") %>
|
|
22
|
+
# # => "Welcome"
|
|
23
|
+
#
|
|
24
|
+
# @example HTML-safe translation
|
|
25
|
+
# # en.yml
|
|
26
|
+
# # greeting_html: "Hello, <strong>%{name}</strong>!"
|
|
27
|
+
# <%= translate("greeting_html", name: "<script>") %>
|
|
28
|
+
# # => "Hello, <strong><script></strong>!" (marked HTML-safe)
|
|
29
|
+
#
|
|
30
|
+
# @example Missing translation
|
|
31
|
+
# <%= translate("missing.key") %>
|
|
32
|
+
# # => '<span class="translation_missing" title="...">missing.key</span>'
|
|
33
|
+
#
|
|
34
|
+
# @example Relative key lookup
|
|
35
|
+
# # In app/templates/users/index.html.erb:
|
|
36
|
+
# <%= translate(".title") %>
|
|
37
|
+
# # Looks up "users.index.title"
|
|
38
|
+
#
|
|
39
|
+
# # In app/templates/users/_form.html.erb (a partial):
|
|
40
|
+
# <%= translate(".label") %>
|
|
41
|
+
# # Looks up "users._form.label"
|
|
42
|
+
#
|
|
43
|
+
# @api public
|
|
44
|
+
# @since 3.0.0
|
|
45
|
+
module I18nHelper
|
|
46
|
+
# Shared `translate` / `localize` (and shorthand) helper methods used by both view-layer
|
|
47
|
+
# consumers and action-layer consumers.
|
|
48
|
+
#
|
|
49
|
+
# **This module is abstract.** Including modules must override two private hooks:
|
|
50
|
+
#
|
|
51
|
+
# - {#_i18n} — returns the i18n backend to delegate to.
|
|
52
|
+
# - {#_resolve_i18n_key} — expands relative (leading-dot) keys against the consumer's
|
|
53
|
+
# context, and is a no-op for absolute keys.
|
|
54
|
+
#
|
|
55
|
+
# @api public
|
|
56
|
+
# @since 3.0.0
|
|
57
|
+
module Methods
|
|
58
|
+
# Matches keys whose final segment is `html` or whose final segment ends in `_html`. These
|
|
59
|
+
# translated values are treated as HTML-safe and any string interpolation values are
|
|
60
|
+
# HTML-escaped before substitution.
|
|
61
|
+
#
|
|
62
|
+
# @api private
|
|
63
|
+
HTML_SAFE_TRANSLATION_KEY = /(\b|_)html\z/
|
|
64
|
+
|
|
65
|
+
# Translates the given key using the slice's i18n backend.
|
|
66
|
+
#
|
|
67
|
+
# When the key's final segment is `html` or ends in `_html`, the result is marked HTML-safe
|
|
68
|
+
# and any string interpolation values are HTML-escaped first.
|
|
69
|
+
#
|
|
70
|
+
# When a translation is missing and neither `:default` nor `:raise` was supplied, returns a
|
|
71
|
+
# `<span class="translation_missing">` element containing the missing key, useful for
|
|
72
|
+
# spotting missing translations during development.
|
|
73
|
+
#
|
|
74
|
+
# When the key begins with a `.`, it is treated as relative to the consumer's context and
|
|
75
|
+
# expanded by {#_resolve_i18n_key}.
|
|
76
|
+
#
|
|
77
|
+
# @param key [String, Symbol] the translation key to look up
|
|
78
|
+
# @param options [Hash] translation options forwarded to the backend (`:locale`, `:scope`,
|
|
79
|
+
# `:default`, `:count`, `:raise`, etc.), plus any interpolation values
|
|
80
|
+
#
|
|
81
|
+
# @return [String] the translated string (marked HTML-safe when hanami-view is bundled and
|
|
82
|
+
# the key ends in `_html` or `html`)
|
|
83
|
+
#
|
|
84
|
+
# @api public
|
|
85
|
+
# @since 3.0.0
|
|
86
|
+
def translate(key, **options)
|
|
87
|
+
key = _resolve_i18n_key(key)
|
|
88
|
+
|
|
89
|
+
html_safe = _html_safe_translation_key?(key)
|
|
90
|
+
|
|
91
|
+
options = _escape_translation_options(options) if html_safe
|
|
92
|
+
|
|
93
|
+
result =
|
|
94
|
+
if options.key?(:default) || options[:raise]
|
|
95
|
+
_i18n.translate(key, **options)
|
|
96
|
+
else
|
|
97
|
+
begin
|
|
98
|
+
_i18n.translate(key, **options, raise: true)
|
|
99
|
+
rescue ::I18n::MissingTranslationData => exception
|
|
100
|
+
return _missing_translation_markup(key, exception)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
html_safe ? _i18n_mark_html_safe(result.to_s) : result
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @api public
|
|
108
|
+
# @since 3.0.0
|
|
109
|
+
alias_method :t, :translate
|
|
110
|
+
|
|
111
|
+
# Translates the given key, raising an exception if the translation is missing.
|
|
112
|
+
#
|
|
113
|
+
# @param (see #translate)
|
|
114
|
+
#
|
|
115
|
+
# @return [String] the translated string (marked HTML-safe when hanami-view is bundled and
|
|
116
|
+
# the key ends in `_html` or `html`)
|
|
117
|
+
#
|
|
118
|
+
# @raise [I18n::MissingTranslationData] if the translation is missing
|
|
119
|
+
#
|
|
120
|
+
# @api public
|
|
121
|
+
# @since 3.0.0
|
|
122
|
+
def translate!(key, **options)
|
|
123
|
+
translate(key, **options, raise: true)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @api public
|
|
127
|
+
# @since 3.0.0
|
|
128
|
+
alias_method :t!, :translate!
|
|
129
|
+
|
|
130
|
+
# Localizes the given object (e.g. a date, time, or number) using the slice's i18n backend.
|
|
131
|
+
#
|
|
132
|
+
# @param object [Date, Time, DateTime, Numeric] the object to localize
|
|
133
|
+
# @param options [Hash] localization options forwarded to the backend (`:locale`, `:format`,
|
|
134
|
+
# etc.)
|
|
135
|
+
#
|
|
136
|
+
# @return [String] the localized string
|
|
137
|
+
#
|
|
138
|
+
# @api public
|
|
139
|
+
# @since 3.0.0
|
|
140
|
+
def localize(object, **options)
|
|
141
|
+
_i18n.localize(object, **options)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @api public
|
|
145
|
+
# @since 3.0.0
|
|
146
|
+
alias_method :l, :localize
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# Returns the i18n backend the helper methods delegate to.
|
|
151
|
+
#
|
|
152
|
+
# Including modules must override this method to return an I18n backend instance.
|
|
153
|
+
#
|
|
154
|
+
# @return [Hanami::Providers::I18n::Backend]
|
|
155
|
+
def _i18n
|
|
156
|
+
raise NoMethodError, "#{self.class} must implement #_i18n to return an i18n backend"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Resolves the given key, returning it unchanged by default.
|
|
160
|
+
#
|
|
161
|
+
# Override this hook to expand relative (leading-dot) keys against a context.
|
|
162
|
+
#
|
|
163
|
+
# @param key [String, Symbol]
|
|
164
|
+
#
|
|
165
|
+
# @return [String, Symbol] the resolved key
|
|
166
|
+
def _resolve_i18n_key(key)
|
|
167
|
+
key
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def _html_safe_translation_key?(key)
|
|
171
|
+
HTML_SAFE_TRANSLATION_KEY.match?(key.to_s)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def _escape_translation_options(options)
|
|
175
|
+
options.each_with_object({}) do |(key, value), result|
|
|
176
|
+
result[key] =
|
|
177
|
+
if ::I18n::RESERVED_KEYS.include?(key) || !value.is_a?(String) || _i18n_html_safe?(value)
|
|
178
|
+
value
|
|
179
|
+
else
|
|
180
|
+
_i18n_html_escape(value)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def _missing_translation_markup(key, error)
|
|
186
|
+
title = _i18n_html_escape(error.message)
|
|
187
|
+
body = _i18n_html_escape(key.to_s)
|
|
188
|
+
_i18n_mark_html_safe(%(<span class="translation_missing" title="#{title}">#{body}</span>))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Escapes `value` for HTML. Prefers `Hanami::View::Helpers::EscapeHelper#escape_html` when
|
|
192
|
+
# Hanami View's EscapeHelper is included, falling back to stdlib `CGI.escapeHTML` so the
|
|
193
|
+
# helper works in API-only apps that don't bundle hanami-view.
|
|
194
|
+
def _i18n_html_escape(value)
|
|
195
|
+
if respond_to?(:escape_html)
|
|
196
|
+
escape_html(value)
|
|
197
|
+
else
|
|
198
|
+
CGI.escapeHTML(value.to_s)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Marks the given string as HTML-safe when Hanami View is loaded, otherwise returns the
|
|
203
|
+
# string unchanged.
|
|
204
|
+
#
|
|
205
|
+
# The HTML-safe marker is only meaningful to Hanami View's template rendering, so there's
|
|
206
|
+
# nothing to do in its absence.
|
|
207
|
+
def _i18n_mark_html_safe(str)
|
|
208
|
+
str.respond_to?(:html_safe) ? str.html_safe : str
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def _i18n_html_safe?(value)
|
|
212
|
+
value.respond_to?(:html_safe?) && value.html_safe?
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
include Methods
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def _i18n
|
|
221
|
+
_context.i18n
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def _resolve_i18n_key(key)
|
|
225
|
+
return key unless key.to_s.start_with?(".")
|
|
226
|
+
|
|
227
|
+
template_name = _context.current_template_name
|
|
228
|
+
|
|
229
|
+
unless template_name
|
|
230
|
+
raise(
|
|
231
|
+
::I18n::ArgumentError,
|
|
232
|
+
"Cannot use relative translation key #{key.inspect} outside of a template render. " \
|
|
233
|
+
"Use an absolute key (without a leading dot) instead."
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
"#{template_name.tr("/", ".")}#{key}"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/logger"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
module Logger
|
|
7
|
+
# Rack request log formatter for Dry Logger.
|
|
8
|
+
#
|
|
9
|
+
# Formats rack request log entries with colorized output for HTTP verbs, status codes, and
|
|
10
|
+
# request paths, making it easier to visually scan logs in development.
|
|
11
|
+
#
|
|
12
|
+
# HTTP verbs each have a distinct color. Status codes follow traffic-light coloring (2xx green,
|
|
13
|
+
# 3xx cyan, 4xx yellow, 5xx red), and the request path echoes the status color so both signals
|
|
14
|
+
# reinforce each other at a glance.
|
|
15
|
+
#
|
|
16
|
+
# Colorization is only active when `colorize: true` is set in the logger options (the default
|
|
17
|
+
# in development).
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
class RackFormatter < Dry::Logger::Formatters::String
|
|
21
|
+
RACK_TEMPLATE = <<~TEXT
|
|
22
|
+
[%<progname>s] [%<severity>s] [%<time>s] \
|
|
23
|
+
%<verb>s %<status>s %<elapsed>s %<ip>s %<path>s %<length>s %<payload>s
|
|
24
|
+
%<params>s
|
|
25
|
+
TEXT
|
|
26
|
+
|
|
27
|
+
VERB_COLORS = {
|
|
28
|
+
"GET" => :green,
|
|
29
|
+
"POST" => :yellow,
|
|
30
|
+
"PUT" => :blue,
|
|
31
|
+
"PATCH" => :blue,
|
|
32
|
+
"DELETE" => :red,
|
|
33
|
+
"HEAD" => :cyan
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
Colors = Dry::Logger::Formatters::Colors
|
|
37
|
+
private_constant :Colors
|
|
38
|
+
|
|
39
|
+
def initialize(**options)
|
|
40
|
+
super
|
|
41
|
+
@template = Dry::Logger::Formatters::Template[RACK_TEMPLATE]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def format_values(entry)
|
|
47
|
+
return super unless colorize?
|
|
48
|
+
|
|
49
|
+
status_color = status_color(entry.to_h[:status])
|
|
50
|
+
|
|
51
|
+
super.merge(
|
|
52
|
+
verb: Colors.call(VERB_COLORS.fetch(entry.to_h[:verb].to_s.upcase, :gray), entry.to_h[:verb]),
|
|
53
|
+
status: Colors.call(status_color, entry.to_h[:status]),
|
|
54
|
+
path: Colors.call(status_color, entry.to_h[:path])
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def format_params(value)
|
|
59
|
+
value unless value.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def status_color(status)
|
|
63
|
+
case status.to_i
|
|
64
|
+
when 200..299 then :green
|
|
65
|
+
when 300..399 then :cyan
|
|
66
|
+
when 400..499 then :yellow
|
|
67
|
+
when 500..599 then :red
|
|
68
|
+
else :gray
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/logger"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
module Logger
|
|
7
|
+
# SQL query log formatter for Dry Logger.
|
|
8
|
+
#
|
|
9
|
+
# Formats SQL query log entries with a template that mirrors the structure of the built-in rack
|
|
10
|
+
# log formatter, providing consistent visual formatting across both HTTP request and database
|
|
11
|
+
# query logs.
|
|
12
|
+
#
|
|
13
|
+
# For example, in development, SQL logs alongside Rack logs:
|
|
14
|
+
#
|
|
15
|
+
# [my_app] [INFO] [2026-03-04 10:15:32] SQL sqlite 1.234ms SELECT * FROM users
|
|
16
|
+
# [my_app] [INFO] [2026-03-04 10:15:32] GET 200 1ms 127.0.0.1 /users -
|
|
17
|
+
#
|
|
18
|
+
# In production, the default JSON formatter handles SQL entries automatically via the structured
|
|
19
|
+
# payload.
|
|
20
|
+
#
|
|
21
|
+
# Supports colorization via Dry Logger's template color tags. When `colorize: true` is set in
|
|
22
|
+
# the logger options (the default in development), the "SQL" label is colorized, and severity
|
|
23
|
+
# is colorized per-level by the parent formatter (e.g. INFO => magenta, ERROR => red).
|
|
24
|
+
#
|
|
25
|
+
# When colorization is enabled and the "rouge" gem is available, SQL queries are syntax
|
|
26
|
+
# highlighted using Rouge's SQL lexer. This is a soft dependency; if Rouge is not installed,
|
|
27
|
+
# queries output as plain, unhighlighted text.
|
|
28
|
+
#
|
|
29
|
+
# The Rouge theme defaults to "pastie" and can be customized by setting the
|
|
30
|
+
# `HANAMI_LOG_SYNTAX_THEME` environment variable to any Rouge theme name. See
|
|
31
|
+
# `Rouge::Theme.registry` for available themes.
|
|
32
|
+
#
|
|
33
|
+
# @see Hanami::Logger::SQLLogger
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
class SQLFormatter < Dry::Logger::Formatters::String
|
|
37
|
+
SQL_TEMPLATE = <<~TEXT
|
|
38
|
+
[%<progname>s] [%<severity>s] [%<time>s] SQL %<db>s %<elapsed>s%<elapsed_unit>s %<query>s
|
|
39
|
+
TEXT
|
|
40
|
+
|
|
41
|
+
SQL_TEMPLATE_COLORIZED = <<~TEXT
|
|
42
|
+
[%<progname>s] [%<severity>s] [%<time>s] <blue>SQL</blue> %<db>s %<elapsed>s%<elapsed_unit>s %<query>s
|
|
43
|
+
TEXT
|
|
44
|
+
|
|
45
|
+
def initialize(**options)
|
|
46
|
+
super
|
|
47
|
+
@template = Dry::Logger::Formatters::Template[
|
|
48
|
+
colorize? ? SQL_TEMPLATE_COLORIZED : SQL_TEMPLATE
|
|
49
|
+
]
|
|
50
|
+
@sql_colorizer = build_sql_colorizer if colorize?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def format_query(value)
|
|
56
|
+
if @sql_colorizer
|
|
57
|
+
@sql_colorizer.call(value)
|
|
58
|
+
else
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_sql_colorizer
|
|
64
|
+
begin
|
|
65
|
+
require "rouge"
|
|
66
|
+
rescue LoadError
|
|
67
|
+
return nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
theme_name = ENV.fetch("HANAMI_LOG_SYNTAX_THEME", "pastie")
|
|
71
|
+
theme_class = Rouge::Theme.find(theme_name) || Rouge::Themes::Pastie
|
|
72
|
+
formatter = Rouge::Formatters::Terminal256.new(theme_class.new)
|
|
73
|
+
|
|
74
|
+
lexer = Rouge::Lexers::SQL.new
|
|
75
|
+
|
|
76
|
+
->(sql) { formatter.format(lexer.lex(sql)) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|