brut 0.0.13 → 0.0.20

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -6
  3. data/brut.gemspec +1 -3
  4. data/lib/brut/back_end.rb +7 -0
  5. data/lib/brut/cli/apps/scaffold.rb +16 -24
  6. data/lib/brut/framework/config.rb +4 -43
  7. data/lib/brut/framework/mcp.rb +1 -1
  8. data/lib/brut/front_end/asset_path_resolver.rb +15 -0
  9. data/lib/brut/front_end/component.rb +66 -234
  10. data/lib/brut/front_end/components/constraint_violations.rb +9 -9
  11. data/lib/brut/front_end/components/form_tag.rb +16 -28
  12. data/lib/brut/front_end/components/i18n_translations.rb +12 -13
  13. data/lib/brut/front_end/components/input.rb +0 -1
  14. data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -2
  15. data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
  16. data/lib/brut/front_end/components/inputs/select.rb +13 -20
  17. data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
  18. data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
  19. data/lib/brut/front_end/components/locale_detection.rb +2 -2
  20. data/lib/brut/front_end/components/page_identifier.rb +3 -5
  21. data/lib/brut/front_end/components/{time.rb → time_tag.rb} +13 -10
  22. data/lib/brut/front_end/components/traceparent.rb +5 -6
  23. data/lib/brut/front_end/http_method.rb +4 -0
  24. data/lib/brut/front_end/inline_svg_locator.rb +21 -0
  25. data/lib/brut/front_end/layout.rb +3 -0
  26. data/lib/brut/front_end/page.rb +16 -29
  27. data/lib/brut/front_end/request_context.rb +13 -0
  28. data/lib/brut/front_end/routing.rb +3 -2
  29. data/lib/brut/front_end.rb +41 -0
  30. data/lib/brut/i18n/base_methods.rb +14 -8
  31. data/lib/brut/i18n/for_back_end.rb +5 -0
  32. data/lib/brut/i18n/for_cli.rb +2 -1
  33. data/lib/brut/i18n/for_html.rb +9 -1
  34. data/lib/brut/i18n.rb +1 -0
  35. data/lib/brut/sinatra_helpers.rb +12 -7
  36. data/lib/brut/spec_support/component_support.rb +9 -9
  37. data/lib/brut/spec_support/e2e_support.rb +4 -0
  38. data/lib/brut/spec_support/matchers/have_i18n_string.rb +5 -0
  39. data/lib/brut/spec_support/rspec_setup.rb +1 -0
  40. data/lib/brut/spec_support.rb +4 -3
  41. data/lib/brut/version.rb +1 -1
  42. data/lib/brut.rb +2 -46
  43. metadata +13 -41
  44. data/lib/brut/front_end/template.rb +0 -47
  45. data/lib/brut/front_end/templates/block_filter.rb +0 -61
  46. data/lib/brut/front_end/templates/erb_engine.rb +0 -26
  47. data/lib/brut/front_end/templates/erb_parser.rb +0 -84
  48. data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
  49. data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
  50. data/lib/brut/front_end/templates/locator.rb +0 -60
@@ -31,27 +31,27 @@ class Brut::FrontEnd::Components::ConstraintViolations < Brut::FrontEnd::Compone
31
31
  @input_name = input_name
32
32
  @array = !index.nil?
33
33
  @index = index || 0
34
- @html_attributes = html_attributes
35
- @message_html_attributes = message_html_attributes
34
+ @html_attributes = html_attributes.map {|name,value| [ name.to_sym, value ] }.to_h
35
+ @message_html_attributes = message_html_attributes.map {|name,value| [ name.to_sym, value ] }.to_h
36
36
  end
37
37
 
38
- def render
38
+ def view_template
39
39
  html_attributes = {
40
- "input-name": @array ? "#{@input_name}[]" : @input_name
40
+ "input-name": @array ? "#{@input_name}[]" : @input_name.to_s,
41
41
  }.merge(@html_attributes)
42
42
 
43
43
  message_html_attributes = {
44
44
  "server-side": true,
45
45
  }.merge(@message_html_attributes)
46
46
 
47
- html_tag("brut-cv-messages", **html_attributes) do
47
+ brut_cv_messages(**html_attributes) do
48
48
  @form.input(@input_name, index: @index).validity_state.select { |constraint|
49
49
  !constraint.client_side?
50
- }.map { |constraint|
51
- html_tag("brut-cv",**message_html_attributes) do
52
- t("cv.be.#{constraint}", **constraint.context).capitalize
50
+ }.each do |constraint|
51
+ brut_cv(**message_html_attributes) do
52
+ t("cv.be.#{constraint}", **constraint.context)
53
53
  end
54
- }.join("\n")
54
+ end
55
55
  end
56
56
  end
57
57
  end
@@ -1,4 +1,3 @@
1
- require "rexml"
2
1
  # Represents a `<form>` HTML element that includes a CSRF token as needed. You likely want to use this class via the {Brut::FrontEnd::Component::Helpers#form_tag} method.
3
2
  class Brut::FrontEnd::Components::FormTag < Brut::FrontEnd::Component
4
3
  # Creates the form surrounding the contents of the block yielded to it. If the form's action is a POST, it will include a CSRF token.
@@ -22,7 +21,7 @@ class Brut::FrontEnd::Components::FormTag < Brut::FrontEnd::Component
22
21
  # @option html_attributes [Class|Brut::FrontEnd::Form] :for the form object or class representing this HTML form *or* the class of a handler the form should submit to. If you pass this, you may not pass the HTML attributes `:action` or `:method`. Both will be derived from this object.
23
22
  # @option html_attributes [String] «any-other-key» attributes to set on the `<form>` tag
24
23
  # @yield No parameters given. This is expected to return additional markup to appear inside the `<form>` element.
25
- def initialize(route_params: {}, **html_attributes,&contents)
24
+ def initialize(route_params: {}, **html_attributes)
26
25
  form_class = html_attributes.delete(:for) # Cannot be a keyword arg, since for is a reserved word
27
26
  if !form_class.nil?
28
27
  if form_class.kind_of?(Brut::FrontEnd::Form)
@@ -49,34 +48,23 @@ class Brut::FrontEnd::Components::FormTag < Brut::FrontEnd::Component
49
48
 
50
49
  @include_csrf_token = http_method.post?
51
50
  @csrf_token_omit_reasoning = http_method.get? ? "because this form's action is a GET" : nil
52
- @attributes = html_attributes
53
- @contents = contents
51
+ @attributes = html_attributes.merge(method: http_method)
54
52
  end
55
53
 
56
- # @!visibility private
57
- def render
58
- attribute_string = @attributes.map { |key,value|
59
- key = key.to_s
60
- if value == true
61
- key
62
- elsif value == false
63
- ""
64
- else
65
- REXML::Attribute.new(key,value).to_string
54
+ def view_template
55
+ form(**@attributes) do
56
+ if @include_csrf_token
57
+ render Brut::FrontEnd::RequestContext.inject(Brut::FrontEnd::Components::Inputs::CsrfToken)
58
+ elsif Brut.container.project_env.development?
59
+ comment do
60
+ "CSRF Token omitted #{@csrf_token_omit_reasoning} (this message only appears in development)"
61
+ end
66
62
  end
67
- }.join(" ")
68
- csrf_token_component = if @include_csrf_token
69
- component(Brut::FrontEnd::Components::Inputs::CsrfToken)
70
- elsif Brut.container.project_env.development?
71
- html_safe!("<!-- CSRF Token omitted #{@csrf_token_omit_reasoning} (this message only appears in development) -->")
72
- else
73
- ""
74
- end
75
- %{
76
- <form #{attribute_string}>
77
- #{ csrf_token_component }
78
- #{ @contents.() }
79
- </form>
80
- }
63
+ if block_given?
64
+ yield
65
+ end
66
+ end
81
67
  end
68
+
69
+
82
70
  end
@@ -1,5 +1,3 @@
1
- require "rexml"
2
-
3
1
  # Produces `<brut-i18n-translation>` entries for the given values. This is used for client-side constraint violation messaging with
4
2
  # JavaScript. The `<brut-constraint-violation-message>` tag uses these keys to produce messages on the client.
5
3
  #
@@ -39,13 +37,13 @@ class Brut::FrontEnd::Components::I18nTranslations < Brut::FrontEnd::Component
39
37
  end
40
38
 
41
39
  # @!visibility private
42
- def render
40
+ def view_template
43
41
  values = ::I18n.t(@i18n_key_root)
44
42
  if values.kind_of?(String)
45
43
  values = { "" => values }
46
44
  end
47
45
 
48
- values.map { |key,value|
46
+ values.each do |key,value|
49
47
  if !value.kind_of?(String)
50
48
  raise "Key #{key} under #{@i18n_key_root} maps to a #{value.class} instead of a String. For #{self.class} to work, the value must be a String"
51
49
  end
@@ -54,16 +52,17 @@ class Brut::FrontEnd::Components::I18nTranslations < Brut::FrontEnd::Component
54
52
  else
55
53
  "#{@i18n_key_root}.#{key}"
56
54
  end
57
- attributes = [
58
- REXML::Attribute.new("key",i18n_key),
59
- REXML::Attribute.new("value",value.to_s),
60
- ]
55
+ attributes = {
56
+ key: i18n_key,
57
+ value: value.to_s,
58
+ }
61
59
  if !Brut.container.project_env.production?
62
- attributes << REXML::Attribute.new("show-warnings",true)
63
- attributes << REXML::Attribute.new("id","brut-18n-#{key}")
60
+ attributes[:show_warnings] = true
61
+ attributes[:id] = "brut-18n-#{key}"
64
62
  end
65
- attribute_string = attributes.map(&:to_string).join(" ")
66
- %{<brut-i18n-translation #{attribute_string}></brut-i18n-translation>}
67
- }.join("\n")
63
+
64
+ brut_i18n_translation(**attributes)
65
+
66
+ end
68
67
  end
69
68
  end
@@ -1,4 +1,3 @@
1
- require "rexml"
2
1
  module Brut::FrontEnd::Components
3
2
 
4
3
  # Holds components designed to render HTML `<input>` and other form components.
@@ -4,7 +4,7 @@ class Brut::FrontEnd::Components::Inputs::CsrfToken < Brut::FrontEnd::Components
4
4
  def initialize(csrf_token:)
5
5
  @csrf_token = csrf_token
6
6
  end
7
- def render
8
- html_tag(:input, type: "hidden", name: "authenticity_token", value: @csrf_token)
7
+ def view_template
8
+ input(type: "hidden", name: "authenticity_token", value: @csrf_token)
9
9
  end
10
10
  end
@@ -14,18 +14,18 @@ class Brut::FrontEnd::Components::Inputs::RadioButton < Brut::FrontEnd::Componen
14
14
  # @param [Hash] html_attributes any additional HTML attributes to include on the `<input>` element.
15
15
  def self.for_form_input(form:, input_name:, value:, html_attributes: {})
16
16
  default_html_attributes = {}
17
- html_attributes = html_attributes.map { |key,value| [ key.to_s, value ] }.to_h
17
+ html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
18
18
  input = form.input(input_name)
19
19
 
20
- default_html_attributes["required"] = input.required
21
- default_html_attributes["type"] = "radio"
22
- default_html_attributes["name"] = input.name
23
- default_html_attributes["value"] = value
20
+ default_html_attributes[:required] = input.required
21
+ default_html_attributes[:type] = "radio"
22
+ default_html_attributes[:name] = input.name
23
+ default_html_attributes[:value] = value
24
24
 
25
25
  selected_value = input.value
26
26
 
27
27
  if selected_value == value
28
- default_html_attributes["checked"] = true
28
+ default_html_attributes[:checked] = true
29
29
  end
30
30
 
31
31
  if !form.new? && !input.valid?
@@ -21,10 +21,11 @@ class Brut::FrontEnd::Components::Inputs::Select < Brut::FrontEnd::Components::I
21
21
  option_text_attribute:,
22
22
  index: nil,
23
23
  html_attributes: {})
24
+ html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
24
25
  default_html_attributes = {}
25
26
  index ||= 0
26
27
  input = form.input(input_name, index:)
27
- default_html_attributes["required"] = input.required
28
+ default_html_attributes[:required] = input.required
28
29
  if !form.new? && !input.valid?
29
30
  default_html_attributes["data-invalid"] = true
30
31
  input.validity_state.each do |constraint,violated|
@@ -61,36 +62,28 @@ class Brut::FrontEnd::Components::Inputs::Select < Brut::FrontEnd::Components::I
61
62
  @selected_value = selected_value
62
63
  @value_attribute = value_attribute
63
64
  @option_text_attribute = option_text_attribute
65
+ @html_attributes = html_attributes
64
66
 
65
- html_attributes["name"] = name
66
- @sanitized_attributes = html_attributes.map { |key,value|
67
- [
68
- key.to_s.gsub(/[\s\"\'>\/=]/,"-"),
69
- value
70
- ]
71
- }.select { |key,value|
72
- !value.nil?
73
- }.to_h
67
+ @html_attributes[:name] = name
74
68
  end
75
69
 
76
- def render
77
- html_tag(:select,**@sanitized_attributes) {
78
- options = @options.map { |option|
70
+ def view_template
71
+ select(**@html_attributes) {
72
+ if @include_blank
73
+ option(**@include_blank.option_attributes) {
74
+ @include_blank.text_content
75
+ }
76
+ end
77
+ options = @options.each do |option|
79
78
  value = option.send(@value_attribute)
80
79
  option_attributes = { value: value }
81
80
  if value == @selected_value
82
81
  option_attributes[:selected] = true
83
82
  end
84
- html_tag(:option,**option_attributes) {
83
+ option(**option_attributes) {
85
84
  option.send(@option_text_attribute)
86
85
  }
87
- }
88
- if @include_blank
89
- options.unshift(html_tag(:option,**@include_blank.option_attributes) {
90
- @include_blank.text_content
91
- })
92
86
  end
93
- options.join("\n")
94
87
  }
95
88
  end
96
89
  private
@@ -9,40 +9,39 @@ class Brut::FrontEnd::Components::Inputs::TextField < Brut::FrontEnd::Components
9
9
  # @param [Hash] html_attributes any additional HTML attributes to include on the `<input>` element.
10
10
  def self.for_form_input(form:, input_name:, index: nil, html_attributes: {})
11
11
  default_html_attributes = {}
12
- html_attributes = html_attributes.map { |key,value| [ key.to_s, value ] }.to_h
12
+ html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
13
13
  input = form.input(input_name, index:)
14
14
 
15
- default_html_attributes["required"] = input.required
16
- default_html_attributes["pattern"] = input.pattern
17
- default_html_attributes["type"] = input.type
18
- default_html_attributes["name"] = if input.array?
15
+ default_html_attributes[:required] = input.required
16
+ default_html_attributes[:pattern] = input.pattern
17
+ default_html_attributes[:type] = input.type
18
+ default_html_attributes[:name] = if input.array?
19
19
  "#{input.name}[]"
20
20
  else
21
21
  input.name
22
22
  end
23
23
 
24
24
  if input.max
25
- default_html_attributes["max"] = input.max
25
+ default_html_attributes[:max] = input.max
26
26
  end
27
27
  if input.maxlength
28
- default_html_attributes["maxlength"] = input.maxlength
28
+ default_html_attributes[:maxlength] = input.maxlength
29
29
  end
30
30
  if input.min
31
- default_html_attributes["min"] = input.min
31
+ default_html_attributes[:min] = input.min
32
32
  end
33
33
  if input.minlength
34
- default_html_attributes["minlength"] = input.minlength
35
- end
34
+ default_html_attributes[:minlength] = input.minlength end
36
35
  if input.step
37
- default_html_attributes["step"] = input.step
36
+ default_html_attributes[:step] = input.step
38
37
  end
39
38
  value = input.value
40
39
 
41
40
  if input.type == "checkbox"
42
- default_html_attributes["value"] = (index || true).to_s
43
- default_html_attributes["checked"] = value == "true"
41
+ default_html_attributes[:value] = (index || true).to_s
42
+ default_html_attributes[:checked] = value == "true"
44
43
  else
45
- default_html_attributes["value"] = value
44
+ default_html_attributes[:value] = value.to_s
46
45
  end
47
46
  if !form.new? && !input.valid?
48
47
  default_html_attributes["data-invalid"] = true
@@ -55,30 +54,16 @@ class Brut::FrontEnd::Components::Inputs::TextField < Brut::FrontEnd::Components
55
54
  Brut::FrontEnd::Components::Inputs::TextField.new(default_html_attributes.merge(html_attributes))
56
55
  end
57
56
 
57
+ def invalid? = @attributes["data-invalid"] == true
58
+
58
59
  # Create an instance
59
60
  #
60
61
  # @param [Hash] attributes HTML attributes to put on the element.
61
62
  def initialize(attributes)
62
- @sanitized_attributes = attributes.map { |key,value|
63
- [
64
- key.to_s.gsub(/[\s\"\'>\/=]/,"-"),
65
- value
66
- ]
67
- }.select { |key,value|
68
- !value.nil?
69
- }.to_h
63
+ @attributes = attributes
70
64
  end
71
65
 
72
- def render
73
- attribute_string = @sanitized_attributes.map { |key,value|
74
- if value == true
75
- key
76
- elsif value == false
77
- ""
78
- else
79
- REXML::Attribute.new(key,value).to_string
80
- end
81
- }.join(" ")
82
- "<input #{attribute_string}>"
66
+ def view_template
67
+ input(**@attributes)
83
68
  end
84
69
  end
@@ -8,22 +8,23 @@ class Brut::FrontEnd::Components::Inputs::Textarea < Brut::FrontEnd::Components:
8
8
  # @param [Integer] index if this input is part of an array, this is the index into that array. This is used to get the input's value.
9
9
  # @param [Hash] html_attributes any additional HTML attributes to include on the `<textarea>` element.
10
10
  def self.for_form_input(form:, input_name:, index: nil, html_attributes: {})
11
+ html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
11
12
  default_html_attributes = {}
12
13
 
13
14
  index ||= 0
14
15
  input = form.input(input_name, index:)
15
16
 
16
- default_html_attributes["required"] = input.required
17
- default_html_attributes["name"] = if input.array?
17
+ default_html_attributes[:required] = input.required
18
+ default_html_attributes[:name] = if input.array?
18
19
  "#{input.name}[]"
19
20
  else
20
21
  input.name
21
22
  end
22
23
  if input.maxlength
23
- default_html_attributes["maxlength"] = input.maxlength
24
+ default_html_attributes[:maxlength] = input.maxlength
24
25
  end
25
26
  if input.minlength
26
- default_html_attributes["minlength"] = input.minlength
27
+ default_html_attributes[:minlength] = input.minlength
27
28
  end
28
29
  if !form.new? && !input.valid?
29
30
  default_html_attributes["data-invalid"] = true
@@ -41,31 +42,15 @@ class Brut::FrontEnd::Components::Inputs::Textarea < Brut::FrontEnd::Components:
41
42
  # @param [Hash] attributes HTML attributes to put on the element.
42
43
  # @param [String] value the value to place inside the text area
43
44
  def initialize(attributes, value)
44
- @sanitized_attributes = attributes.map { |key,value|
45
- [
46
- key.to_s.gsub(/[\s\"\'>\/=]/,"-"),
47
- value
48
- ]
49
- }.select { |key,value|
50
- !value.nil?
51
- }.to_h
52
- @value = value
45
+ @attributes = attributes
46
+ @value = value
53
47
  end
54
48
 
55
- def sanitized_attributes = @sanitized_attributes
49
+ def invalid? = @attributes["data-invalid"] == true
56
50
 
57
- def render
58
- attribute_string = @sanitized_attributes.map { |key,value|
59
- if value == true
60
- key
61
- elsif value == false
62
- ""
63
- else
64
- REXML::Attribute.new(key,value).to_string
65
- end
66
- }.join(" ")
67
- %{
68
- <textarea #{attribute_string}>#{ @value }</textarea>
51
+ def view_template
52
+ textarea(**@attributes) {
53
+ @value
69
54
  }
70
55
  end
71
56
  end
@@ -13,7 +13,7 @@ class Brut::FrontEnd::Components::LocaleDetection < Brut::FrontEnd::Component
13
13
  @url = Brut::FrontEnd::Handlers::LocaleDetectionHandler.routing
14
14
  end
15
15
 
16
- def render
16
+ def view_template
17
17
  attributes = {
18
18
  "url" => @url,
19
19
  }
@@ -27,6 +27,6 @@ class Brut::FrontEnd::Components::LocaleDetection < Brut::FrontEnd::Component
27
27
  attributes["show-warnings"] = true
28
28
  end
29
29
 
30
- html_tag("brut-locale-detection",**attributes)
30
+ brut_locale_detection(**attributes)
31
31
  end
32
32
  end
@@ -1,15 +1,13 @@
1
- require "rexml"
2
-
3
1
  # Renders a `<meta>` tag that contains the name of the page. This is useful for end to end tests to assert that they are on a specific page before continuing with the test. It can eliminate a lot of confusion when a test fails.
4
2
  class Brut::FrontEnd::Components::PageIdentifier < Brut::FrontEnd::Component
5
3
  def initialize(page_name)
6
4
  @page_name = page_name
7
5
  end
8
6
 
9
- def render
7
+ def view_template
10
8
  if Brut.container.project_env.production?
11
- return ""
9
+ return nil
12
10
  end
13
- html_tag(:meta, name: "class", content: @page_name)
11
+ meta(name: "class", content: @page_name)
14
12
  end
15
13
  end
@@ -1,6 +1,5 @@
1
- require "rexml"
2
1
  # Renders a date or timestamp accessibly, using the `<time>` element. Likely you will use this via the {Brut::FrontEnd::Component::Helpers#time_tag} method. This will account for the current request's time zone. See {Clock}.
3
- class Brut::FrontEnd::Components::Time < Brut::FrontEnd::Component
2
+ class Brut::FrontEnd::Components::TimeTag < Brut::FrontEnd::Component
4
3
  include Brut::I18n::ForHTML
5
4
  # Creates the component
6
5
  # @param timestamp [Time] the timestamp you wish to render. Mutually exclusive with `date`.
@@ -21,13 +20,18 @@ class Brut::FrontEnd::Components::Time < Brut::FrontEnd::Component
21
20
  skip_year_if_same: true,
22
21
  skip_dow_if_not_this_week: true,
23
22
  attribute_format: :iso_8601,
24
- **only_contains_class,
25
- &contents
23
+ clock: :from_request_context,
24
+ **only_contains_class
26
25
  )
27
26
  require_exactly_one!(timestamp:,date:)
28
27
 
29
28
  @date_only = timestamp.nil?
30
29
  @timestamp = timestamp || date
30
+ @clock = if clock == :from_request_context
31
+ Brut::FrontEnd::RequestContext.current[:clock]
32
+ else
33
+ clock
34
+ end
31
35
 
32
36
  formats = [ format ]
33
37
  use_no_year = skip_year_if_same && @timestamp.year == Time.now.year
@@ -69,21 +73,20 @@ class Brut::FrontEnd::Components::Time < Brut::FrontEnd::Component
69
73
  @format = found_format.to_sym
70
74
  @attribute_format = attribute_format.to_sym
71
75
  @class_attribute = only_contains_class[:class] || ""
72
- @contents = contents
73
76
  end
74
77
 
75
- def render(clock:)
78
+ def view_template
76
79
  adjusted_value = if @date_only
77
80
  @timestamp
78
81
  else
79
- clock.in_time_zone(@timestamp)
82
+ @clock.in_time_zone(@timestamp)
80
83
  end
81
84
 
82
85
  datetime_attribute = ::I18n.l(adjusted_value,format: @attribute_format)
83
86
 
84
- html_tag(:time, class: @class_attribute, datetime: datetime_attribute) do
85
- if @contents
86
- @contents.()
87
+ time(class: @class_attribute, datetime: datetime_attribute) do
88
+ if block_given?
89
+ yield
87
90
  else
88
91
  ::I18n.l(adjusted_value,format: @format)
89
92
  end
@@ -8,15 +8,14 @@ class Brut::FrontEnd::Components::Traceparent < Brut::FrontEnd::Component
8
8
  @traceparent = carrier["traceparent"]
9
9
  end
10
10
 
11
- def render
11
+ def view_template
12
12
  attributes = {
13
- name: "traceparent"
13
+ name: "traceparent",
14
+ content: @traceparent,
14
15
  }
15
- if @traceparent
16
- attributes[:content] = @traceparent
17
- else
16
+ if !@traceparent
18
17
  attributes["data-no-traceparent"] = "no traceparent was available - this component may have been rendered outside of an existing OpenTelemetry context"
19
18
  end
20
- html_tag(:meta, **attributes)
19
+ meta(**attributes)
21
20
  end
22
21
  end
@@ -1,5 +1,9 @@
1
+ require "phlex"
2
+
1
3
  # Wrapper around an HTTP Method, ensuring it contains only a valid value.
2
4
  class Brut::FrontEnd::HttpMethod
5
+ include Phlex::SGML::SafeObject
6
+
3
7
  # Create an HTTP method from a string.
4
8
  #
5
9
  # @param [String|Symbol] string a string containing an HTTP method name. Case insensitive, and can be a symbol.
@@ -0,0 +1,21 @@
1
+ class Brut::FrontEnd::InlineSvgLocator
2
+ def initialize(paths:)
3
+ @paths = Array(paths).map { |path| Pathname(path) }
4
+ end
5
+
6
+ def locate(base_name)
7
+ paths_to_try = @paths.map { |path|
8
+ path / "#{base_name}.svg"
9
+ }
10
+ paths_found = paths_to_try.select { |path|
11
+ path.exist?
12
+ }
13
+ if paths_found.empty?
14
+ raise "Could not locate SVG for #{base_name}. Tried: #{paths_to_try.map(&:to_s).join(', ')}"
15
+ end
16
+ if paths_found.length > 1
17
+ raise "Found more than one valid path for #{base_name}. You must rename your files to disambiguate them. These paths were all found: #{paths_found.map(&:to_s).join(', ')}"
18
+ end
19
+ return paths_found[0]
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ class Brut::FrontEnd::Layout < Brut::FrontEnd::Component
2
+ def asset_path(path) = Brut.container.asset_path_resolver.resolve(path)
3
+ end
@@ -14,7 +14,6 @@
14
14
  # @see Brut::FrontEnd::Component
15
15
  class Brut::FrontEnd::Page < Brut::FrontEnd::Component
16
16
  include Brut::FrontEnd::HandlingResults
17
- using Brut::FrontEnd::Templates::HTMLSafeString::Refinement
18
17
 
19
18
  # Returns the name of the layout for this page. This string is used to find an ERB file in `app/src/front_end/layouts`. Every page
20
19
  # must have a layout. If you wish to render a page with no layout, create an empty layout in your app and use that.
@@ -31,43 +30,35 @@ class Brut::FrontEnd::Page < Brut::FrontEnd::Component
31
30
  # @return [URI|Brut::FrontEnd::HttpStatus|Object] If you return a `URI` (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#redirect_to}), the user is redirected and {#render} is never called. If you return a {Brut::FrontEnd::HttpStatus} (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#http_status}), {#render} is skipped and that status is returned with no content. If anything else is returned, {#render} is called as normal.
32
31
  def before_render = nil
33
32
 
34
- # @!visibility private
33
+ def with_layout(&block)
34
+ layout_class = Module.const_get(
35
+ layout_class = RichString.new([
36
+ self.layout,
37
+ "layout"
38
+ ].join("_")).camelize
39
+ )
40
+ render layout_class.new(page_name:,&block)
41
+ end
42
+
43
+
35
44
  def handle!
36
45
  case before_render
37
46
  in URI => uri
38
- Brut.container.instrumentation.add_event("before_render got a URI", uri: uri)
39
47
  uri
40
48
  in Brut::FrontEnd::HttpStatus => http_status
41
- Brut.container.instrumentation.add_event("before_render got status", http_status: http_status)
42
49
  http_status
43
50
  else
44
- render
51
+ self.call
45
52
  end
46
53
  end
47
54
 
48
- # The core method of a page, which overrides {Brut::FrontEnd::Component#render}. This is expected to return
49
- # a string to be sent as a response to an HTTP request. Generally, you should not call this method as it is
50
- # called by Brut when your page is requested.
51
- #
52
- # Also, generally don't override this unles you need to do something unusual. Overriding this will completely bypass the layout
53
- # system and skip all ERB processing. Unlike {Brut::FrontEnd::Component#render}, overriding this method does not provide access to injected data from the request context.
54
- #
55
- # @return [Brut::FrontEnd::Templates::HTMLSafeString] string containing the page's full HTML.
56
- def render
57
- layout_template = Brut.container.layout_locator.locate(self.layout).
58
- then { |layout_erb_file| Brut::FrontEnd::Template.new(layout_erb_file) }
59
-
60
- template = Brut.container.page_locator.locate(self.template_name).
61
- then { |erb_file| Brut::FrontEnd::Template.new(erb_file) }
62
-
63
- Brut.container.instrumentation.add_event("templates found", layout: layout_template.template_file_path, page: template.template_file_path)
64
-
65
- page = template.render_template(self).html_safe!
66
- layout_template.render_template(self) do
67
- page
55
+ def view_template
56
+ with_layout do
57
+ page_template
68
58
  end
69
59
  end
70
60
 
61
+
71
62
  # @return [String] name of this page for use in debugging or for whatever reason you may want to dynamically refer to the page's name. The default value is the class name.
72
63
  def self.page_name = self.name
73
64
 
@@ -77,10 +68,6 @@ class Brut::FrontEnd::Page < Brut::FrontEnd::Component
77
68
  # @!visibility private
78
69
  def component_name = raise Brut::Framework::Errors::Bug,"#{self.class} is not a component"
79
70
 
80
- private
81
-
82
- def template_name = RichString.new(self.class.name).underscorized.to_s.gsub(/^pages\//,"")
83
-
84
71
  end
85
72
 
86
73
  # Holds pages included with the Brut framework