brut 0.0.12 → 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 (52) 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/generic_response.rb +33 -0
  24. data/lib/brut/front_end/handler.rb +2 -0
  25. data/lib/brut/front_end/http_method.rb +4 -0
  26. data/lib/brut/front_end/inline_svg_locator.rb +21 -0
  27. data/lib/brut/front_end/layout.rb +3 -0
  28. data/lib/brut/front_end/page.rb +16 -29
  29. data/lib/brut/front_end/request_context.rb +15 -0
  30. data/lib/brut/front_end/routing.rb +3 -2
  31. data/lib/brut/front_end.rb +41 -0
  32. data/lib/brut/i18n/base_methods.rb +14 -8
  33. data/lib/brut/i18n/for_back_end.rb +5 -0
  34. data/lib/brut/i18n/for_cli.rb +2 -1
  35. data/lib/brut/i18n/for_html.rb +9 -1
  36. data/lib/brut/i18n.rb +1 -0
  37. data/lib/brut/sinatra_helpers.rb +14 -7
  38. data/lib/brut/spec_support/component_support.rb +9 -9
  39. data/lib/brut/spec_support/e2e_support.rb +4 -0
  40. data/lib/brut/spec_support/matchers/have_i18n_string.rb +5 -0
  41. data/lib/brut/spec_support/rspec_setup.rb +1 -0
  42. data/lib/brut/spec_support.rb +4 -3
  43. data/lib/brut/version.rb +1 -1
  44. data/lib/brut.rb +2 -45
  45. metadata +16 -43
  46. data/lib/brut/front_end/template.rb +0 -47
  47. data/lib/brut/front_end/templates/block_filter.rb +0 -61
  48. data/lib/brut/front_end/templates/erb_engine.rb +0 -26
  49. data/lib/brut/front_end/templates/erb_parser.rb +0 -84
  50. data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
  51. data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
  52. data/lib/brut/front_end/templates/locator.rb +0 -60
@@ -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