phlex 1.9.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of phlex might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b68a563745c2782c8fa9f864f1d00994d6f5b3b1ffb92d651a52875386587fb
4
- data.tar.gz: 4e272a581c83f91a4ba0e0a382bb2f54f3c7b3918e88b6c232d503f407046e75
3
+ metadata.gz: 6cb979c6d7c51d88b32f291de0ca3133594a03e70615b17c7fe7bff3b4b3e8c6
4
+ data.tar.gz: 4649c3a4526638bbbde4c627c70d57e969e9924cf4d2fac9706ab01d1283faa0
5
5
  SHA512:
6
- metadata.gz: 4db12efca6e80561f266b3118a401bb81c2859a5b6b4f3f6db301ea711c468b1ad426a59db9dc0e9ea02056e60e7c325223051dd2e4a2309dc14d1ffc45410ac
7
- data.tar.gz: 7c6fe13e7fa6e88d4c1857d4746faae3b622586823cfe6e64a71706efa9aca7ed1e82c9c13f5916f1c6780ab914d6a3576d4cd8571df0df249767bbf0c3b86de
6
+ metadata.gz: aaf25254c000f2bc39fb949427bfcced283df5c0dd4b44284ae30ba830d8141d6789adde5548911cb4b381ff096a7a31cc5d877d55cc7d48dfaf650670f08232
7
+ data.tar.gz: f8714c5084425f13c1ebcc7fcdf1e436a6a4ce501568dcf6522e7fd10279dd2902242e3b38ef4d1d1499d316237483eadf65f143718161676677d54c1c8ff65f
data/.rubocop.yml CHANGED
@@ -13,3 +13,15 @@ Style/MixinUsage:
13
13
 
14
14
  Style/RedundantDoubleSplatHashBraces:
15
15
  Enabled: false
16
+
17
+ Style/OptionalArguments:
18
+ Enabled: false
19
+
20
+ Naming/AsciiIdentifiers:
21
+ Enabled: false
22
+
23
+ Naming/MethodName:
24
+ Enabled: false
25
+
26
+ Style/ReturnNilInPredicateMethodDefinition:
27
+ Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.2
1
+ 3.3.0
data/.solargraph.yml ADDED
@@ -0,0 +1,11 @@
1
+ include:
2
+ - "**/*.rb"
3
+ exclude:
4
+ - test/**/*
5
+ require: []
6
+ domains: []
7
+ reporters:
8
+ - rubocop
9
+ - require_not_found
10
+ - typecheck
11
+ max_files: 0
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [1.10.0] 2024-04-05
6
+
7
+ - [Added] new `Phlex::CSV` class for streaming CSV views
8
+ - [Added] new (experimental) `Phlex::Kit` for collections of components
9
+ - [Added] support for selective rendering
10
+ - [Changed] `mix` does a better job when mixing different types of attributes
11
+ - [Changed] Phlex will now try to call `to_s` on attribute values
12
+ - [Changed] No runtime dependencies
13
+ - [Deprecated] `Phlex::HTML#param`, (`<param>`) tags have been deprecated
14
+ - [Deprecated] Defining the `template` method is now deprecated. You should define `view_template` instead. In Phlex 2.0, the `template` method will render a `<template>` tag.
15
+
16
+ # [1.9.1] 2024-03-11
17
+
18
+ - Security update
19
+
5
20
  ## [1.9.0] 2024-11-24
6
21
 
7
22
  - Improved documentation
@@ -42,6 +57,6 @@ All notable changes to this project will be documented in this file. The format
42
57
  - Removed the `menuitem` element as it's a deprecated HTML element.
43
58
  - Removed the `SGML#text` method. This has been replaced with `SGML#plain`.
44
59
 
45
- ***
60
+ ---
46
61
 
47
62
  Before this changelog was introduced, changes were logged in the [release notes](https://github.com/phlex-ruby/phlex/releases).
data/Gemfile CHANGED
@@ -5,13 +5,17 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
5
 
6
6
  gemspec
7
7
 
8
- gem "rubocop"
9
- gem "sus"
10
- gem "benchmark-ips"
11
- gem "yard"
12
- gem "green_dots", github: "joeldrapper/green_dots"
13
-
14
8
  group :test do
15
- gem "i18n"
16
- gem "memory_profiler"
9
+ gem "sus"
10
+ if RUBY_ENGINE == "ruby" && RUBY_VERSION[0] > "3"
11
+ gem "async"
12
+ end
13
+ gem "concurrent-ruby"
14
+ end
15
+
16
+ group :development do
17
+ gem "rubocop"
18
+ gem "solargraph"
19
+ gem "yard"
20
+ gem "benchmark-ips"
17
21
  end
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
- <a href="https://www.phlex.fun"><img alt="Phlex logo" src="https://www.phlex.fun/assets/logo.png" width="180" /></a>
1
+ <a href="https://www.phlex.fun/"><img alt="Phlex logo" src="https://www.phlex.fun/assets/logo.png" width="180" /></a>
2
2
 
3
3
  Phlex lets you compose web views in pure Ruby — kind of like JSX, but not really anything like JSX. It’s super-fast, thread-safe and supports TruffleRuby v22.2+, JRuby v9.2+ and MRI v2.7+. Phlex currently supports [HTML](https://rubydoc.info/gems/phlex/Phlex/HTML) and [SVG](https://rubydoc.info/gems/phlex/Phlex/SVG) views, and we’re exploring JSON and XML.
4
4
 
5
+ Docs and more at [Phlex.fun](https://www.phlex.fun/)
6
+
5
7
  ### Prior Art 🎨
6
8
 
7
9
  - [markaby](https://github.com/markaby/markaby)
data/SECURITY.md CHANGED
@@ -1,13 +1,14 @@
1
- # Security Policy
1
+ # Security
2
2
 
3
- ## Reporting a vulnerability
3
+ If you find a possible security vulnerability, please [send us a private advisory](https://github.com/phlex-ruby/phlex/security/advisories/new).
4
4
 
5
- If you find a possible security vulnerability, please email security@phlex.fun. Do not create an issue or pull request either demonstrating or fixing the vulnerability.
5
+ > [!WARNING]
6
+ > Please do not open a public Issue or Pull Request.
6
7
 
7
8
  ## Bug bounty
8
9
 
9
- [The Gem Foundation](https://ryanbigg.com/2022/11/the-gem-foundation) has kindly sponsored a $1 bug bounty to discover security vulnerabilities in Phlex.
10
+ There is currently a bounty of $100 USD, kindly sponsored by [Seth Horsley](https://twitter.com/SethHorsley), for the next serious vulnerability responsibly disclosed to us.
10
11
 
11
- ## Sponsoring a bug bounty
12
+ ## Bug bounty pot
12
13
 
13
- If you wish to sponsor a bug bounty for Phlex, please get in touch with Joel at joel@drapper.me.
14
+ If you wish to sponsor a bug bounty for Phlex, please get in touch with Joel at [joel@drapper.me](mailto:joel@drapper.me).
data/config/sus.rb CHANGED
@@ -6,5 +6,3 @@ require "bundler"
6
6
  Bundler.require :test
7
7
 
8
8
  require_relative "../fixtures/view_helper"
9
-
10
- Zeitwerk::Loader.eager_load_all
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Components::SayHi < Phlex::HTML
4
+ def initialize(name, times: 1)
5
+ @name = name
6
+ @times = times
7
+ end
8
+
9
+ def template
10
+ article {
11
+ @times.times { h1 { "Hi #{@name}" } }
12
+ yield
13
+ }
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Components
4
+ extend Phlex::Kit
5
+
6
+ autoload :SayHi, "components/say_hi"
7
+ end
data/fixtures/layout.rb CHANGED
@@ -6,7 +6,7 @@ module Example
6
6
  @title = title
7
7
  end
8
8
 
9
- def template(&block)
9
+ def view_template(&block)
10
10
  html do
11
11
  head do
12
12
  title { @title }
data/fixtures/page.rb CHANGED
@@ -2,30 +2,32 @@
2
2
 
3
3
  module Example
4
4
  class Page < Phlex::HTML
5
- def template
5
+ def view_template
6
6
  render LayoutComponent.new do
7
7
  h1 { "Hi" }
8
8
 
9
- table id: "test", class: "a b c d e f g" do
10
- tr do
11
- td id: "test", class: "a b c d e f g" do
12
- span { "Hi" }
13
- end
9
+ 100.times do
10
+ table id: "test", class: "a b c d e f g" do
11
+ tr do
12
+ td id: "test", class: "a b c d e f g" do
13
+ span { "Hi" }
14
+ end
14
15
 
15
- td id: "test", class: "a b c d e f g" do
16
- span { "Hi" }
17
- end
16
+ td id: "test", class: "a b c d e f g" do
17
+ span { "Hi" }
18
+ end
18
19
 
19
- td id: "test", class: "a b c d e f g" do
20
- span { "Hi" }
21
- end
20
+ td id: "test", class: "a b c d e f g" do
21
+ span { "Hi" }
22
+ end
22
23
 
23
- td id: "test", class: "a b c d e f g" do
24
- span { "Hi" }
25
- end
24
+ td id: "test", class: "a b c d e f g" do
25
+ span { "Hi" }
26
+ end
26
27
 
27
- td id: "test", class: "a b c d e f g" do
28
- span { "Hi" }
28
+ td id: "test", class: "a b c d e f g" do
29
+ span { "Hi" }
30
+ end
29
31
  end
30
32
  end
31
33
  end
@@ -2,9 +2,21 @@
2
2
 
3
3
  include TestHelper
4
4
 
5
+ class ToPhlexAttributeValueable
6
+ def to_phlex_attribute_value
7
+ "to_phlex_attribute_value"
8
+ end
9
+ end
10
+
11
+ class ToSable
12
+ def to_s
13
+ "to_s"
14
+ end
15
+ end
16
+
5
17
  class ToStrable
6
18
  def to_str
7
- "foo"
19
+ "to_str"
8
20
  end
9
21
  end
10
22
 
@@ -40,12 +52,28 @@ test "with a set of symbols and strings" do
40
52
  expect(component.new).to_render %(<div class="bg-red-500 rounded"></div>)
41
53
  end
42
54
 
55
+ test "with a to_phlex_attribute_value-able object" do
56
+ component = build_component_with_template do
57
+ div class: ToPhlexAttributeValueable.new
58
+ end
59
+
60
+ expect(component.new).to_render %(<div class="to_phlex_attribute_value"></div>)
61
+ end
62
+
63
+ test "with a to_s-able object" do
64
+ component = build_component_with_template do
65
+ div class: ToSable.new
66
+ end
67
+
68
+ expect(component.new).to_render %(<div class="to_s"></div>)
69
+ end
70
+
43
71
  test "with a to_str-able object" do
44
72
  component = build_component_with_template do
45
73
  div class: ToStrable.new
46
74
  end
47
75
 
48
- expect(component.new).to_render %(<div class="foo"></div>)
76
+ expect(component.new).to_render %(<div class="to_str"></div>)
49
77
  end
50
78
 
51
79
  test "with numeric integer/float" do
@@ -8,7 +8,7 @@ module Phlex::BlackHole
8
8
  self
9
9
  end
10
10
 
11
- def length
11
+ def bytesize
12
12
  0
13
13
  end
14
14
 
data/lib/phlex/context.rb CHANGED
@@ -2,26 +2,63 @@
2
2
 
3
3
  # @api private
4
4
  class Phlex::Context
5
- def initialize
6
- @target = +""
5
+ def initialize(user_context = {})
6
+ @buffer = +""
7
7
  @capturing = false
8
+ @user_context = user_context
9
+ @fragments = nil
10
+ @in_target_fragment = false
11
+ @halt_signal = nil
8
12
  end
9
13
 
10
- attr_accessor :target, :capturing
14
+ attr_accessor :buffer, :capturing, :user_context, :in_target_fragment
11
15
 
12
- def capturing_into(new_target)
13
- original_target = @target
16
+ attr_reader :fragments
17
+
18
+ # Added for backwards compatibility with phlex-rails. We can remove this with 2.0
19
+ def target
20
+ @buffer
21
+ end
22
+
23
+ def target_fragments(fragments)
24
+ @fragments = fragments.to_h { |it| [it, true] }
25
+ end
26
+
27
+ def around_render
28
+ return yield if !@fragments || @halt_signal
29
+
30
+ catch do |signal|
31
+ @halt_signal = signal
32
+ yield
33
+ end
34
+ end
35
+
36
+ def begin_target(id)
37
+ @in_target_fragment = id
38
+ end
39
+
40
+ def end_target
41
+ @fragments.delete(@in_target_fragment)
42
+ @in_target_fragment = false
43
+ throw @halt_signal if @fragments.length == 0
44
+ end
45
+
46
+ def capturing_into(new_buffer)
47
+ original_buffer = @buffer
14
48
  original_capturing = @capturing
49
+ original_fragments = @fragments
15
50
 
16
51
  begin
17
- @target = new_target
52
+ @buffer = new_buffer
18
53
  @capturing = true
54
+ @fragments = nil
19
55
  yield
20
56
  ensure
21
- @target = original_target
57
+ @buffer = original_buffer
22
58
  @capturing = original_capturing
59
+ @fragments = original_fragments
23
60
  end
24
61
 
25
- new_target
62
+ new_buffer
26
63
  end
27
64
  end
data/lib/phlex/csv.rb ADDED
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Phlex::CSV
4
+ include Phlex::Callable
5
+
6
+ FORMULA_PREFIXES = ["=", "+", "-", "@", "\t", "\r"].to_h { |prefix| [prefix, true] }.freeze
7
+ SPACE_CHARACTERS = [" ", "\t", "\r"].to_h { |char| [char, true] }.freeze
8
+
9
+ def initialize(collection)
10
+ @collection = collection
11
+ @_headers = []
12
+ @_current_row = []
13
+ @_current_column_index = 0
14
+ @_view_context = nil
15
+ @_first = true
16
+ end
17
+
18
+ attr_reader :collection
19
+
20
+ def call(buffer = +"", view_context: nil)
21
+ unless escape_csv_injection? == true || escape_csv_injection? == false
22
+ raise <<~MESSAGE
23
+ You need to define escape_csv_injection? in #{self.class.name}, returning either `true` or `false`.
24
+
25
+ CSV injection is a security vulnerability where malicious spreadsheet formulae are used to execute code or exfiltrate data when a CSV is opened in a spreadsheet program such as Microsoft Excel or Google Sheets.
26
+
27
+ For more information, see https://owasp.org/www-community/attacks/CSV_Injection
28
+
29
+ If you're sure this CSV will never be opened in a spreadsheet program, you can disable CSV injection escapes:
30
+
31
+ def escape_csv_injection? = false
32
+
33
+ This is useful when using CSVs for byte-for-byte data exchange between secure systems.
34
+
35
+ Alternatively, you can enable CSV injection escapes at the cost of data integrity:
36
+
37
+ def escape_csv_injection? = true
38
+
39
+ Note: Enabling the CSV injection escapes will prefix any values that start with `=`, `+`, `-`, `@`, `\\t`, or `\\r` with a single quote `'` to prevent them from being interpreted as formulae by spreadsheet programs.
40
+
41
+ Unfortunately, there is no one-size-fits-all solution to CSV injection. You need to decide based on your specific use case.
42
+ MESSAGE
43
+ end
44
+
45
+ @_view_context = view_context
46
+
47
+ each_item do |record|
48
+ yielder(record) do |*args, **kwargs|
49
+ view_template(*args, **kwargs)
50
+
51
+ if @_first && render_headers?
52
+ buffer << @_headers.join(",") << "\n"
53
+ end
54
+
55
+ buffer << @_current_row.join(",") << "\n"
56
+ @_current_column_index = 0
57
+ @_current_row.clear
58
+ end
59
+
60
+ @_first = false
61
+ end
62
+
63
+ buffer
64
+ end
65
+
66
+ def filename
67
+ nil
68
+ end
69
+
70
+ def content_type
71
+ "text/csv"
72
+ end
73
+
74
+ private
75
+
76
+ def column(header = nil, value)
77
+ if @_first
78
+ @_headers << escape(header)
79
+ elsif header != @_headers[@_current_column_index]
80
+ raise "Inconsistent header."
81
+ end
82
+
83
+ @_current_row << escape(value)
84
+ @_current_column_index += 1
85
+ end
86
+
87
+ def each_item(&block)
88
+ collection.each(&block)
89
+ end
90
+
91
+ def yielder(record)
92
+ yield(record)
93
+ end
94
+
95
+ def template(...)
96
+ nil
97
+ end
98
+
99
+ # Override and set to `false` to disable rendering headers.
100
+ def render_headers?
101
+ true
102
+ end
103
+
104
+ # Override and set to `true` to strip leading and trailing whitespace from values.
105
+ def trim_whitespace?
106
+ false
107
+ end
108
+
109
+ # Override and set to `false` to disable CSV injection escapes or `true` to enable.
110
+ def escape_csv_injection?
111
+ nil
112
+ end
113
+
114
+ def helpers
115
+ @_view_context
116
+ end
117
+
118
+ def escape(value)
119
+ value = trim_whitespace? ? value.to_s.strip : value.to_s
120
+ first_char = value[0]
121
+ last_char = value[-1]
122
+
123
+ if escape_csv_injection? && FORMULA_PREFIXES[first_char]
124
+ # Prefix a single quote to prevent Excel, Google Docs, etc. from interpreting the value as a formula.
125
+ # See https://owasp.org/www-community/attacks/CSV_Injection
126
+ %("'#{value.gsub('"', '""')}")
127
+ elsif (!trim_whitespace? && (SPACE_CHARACTERS[first_char] || SPACE_CHARACTERS[last_char])) || value.include?('"') || value.include?(",") || value.include?("\n")
128
+ %("#{value.gsub('"', '""')}")
129
+ else
130
+ value
131
+ end
132
+ end
133
+ end
@@ -11,7 +11,7 @@
11
11
  # @tabs = []
12
12
  # end
13
13
  #
14
- # def template
14
+ # def view_template
15
15
  # @tabs.each { |t| a { t.name } }
16
16
  # @tabs.each { |t| article(&t.content) }
17
17
  # end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
4
- using Phlex::Overrides::Symbol::Name
5
- end
6
-
7
3
  # Extending this module provides the {register_element} macro for registering your own custom elements. It's already extended by {HTML} and {SVG}.
8
4
  # @example
9
5
  # module MyCustomElements
@@ -15,14 +11,14 @@ end
15
11
  # class MyComponent < Phlex::HTML
16
12
  # include MyCustomElements
17
13
  #
18
- # def template
14
+ # def view_template
19
15
  # trix_editor
20
16
  # end
21
17
  # end
22
18
  module Phlex::Elements
23
19
  # @api private
24
20
  def registered_elements
25
- @registered_elements ||= Concurrent::Map.new
21
+ @registered_elements ||= {}
26
22
  end
27
23
 
28
24
  # Register a custom element. This macro defines an element method for the current class and descendents only. There is no global element registry.
@@ -32,35 +28,64 @@ module Phlex::Elements
32
28
  # @note The methods defined by this macro depend on other methods from {SGML} so they should always be mixed into an {HTML} or {SVG} component.
33
29
  # @example Register the custom element `<trix-editor>`
34
30
  # register_element :trix_editor
35
- def register_element(method_name, tag: nil)
36
- tag ||= method_name.name.tr("_", "-")
31
+ def register_element(method_name, tag: method_name.name.tr("_", "-"), deprecated: false)
32
+ if deprecated
33
+ deprecation = <<~RUBY
34
+ Kernel.warn "#{deprecated}"
35
+ RUBY
36
+ else
37
+ deprecation = ""
38
+ end
37
39
 
38
40
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
39
41
  # frozen_string_literal: true
40
42
 
41
43
  def #{method_name}(**attributes, &block)
42
- target = @_context.target
44
+ #{deprecation}
45
+
46
+ context = @_context
47
+ buffer = context.buffer
48
+ fragment = context.fragments
49
+ target_found = false
50
+
51
+ if fragment
52
+ return if fragment.length == 0 # we found all our fragments already
53
+
54
+ id = attributes[:id]
55
+
56
+ if !context.in_target_fragment
57
+ if fragment[id]
58
+ context.begin_target(id)
59
+ target_found = true
60
+ else
61
+ yield(self) if block
62
+ return nil
63
+ end
64
+ end
65
+ end
43
66
 
44
67
  if attributes.length > 0 # with attributes
45
68
  if block # with content block
46
- target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << ">"
69
+ buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << ">"
47
70
  yield_content(&block)
48
- target << "</#{tag}>"
71
+ buffer << "</#{tag}>"
49
72
  else # without content block
50
- target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << "></#{tag}>"
73
+ buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << "></#{tag}>"
51
74
  end
52
75
  else # without attributes
53
76
  if block # with content block
54
- target << "<#{tag}>"
77
+ buffer << "<#{tag}>"
55
78
  yield_content(&block)
56
- target << "</#{tag}>"
79
+ buffer << "</#{tag}>"
57
80
  else # without content block
58
- target << "<#{tag}></#{tag}>"
81
+ buffer << "<#{tag}></#{tag}>"
59
82
  end
60
83
  end
61
84
 
62
85
  #{'flush' if tag == 'head'}
63
86
 
87
+ context.end_target if target_found
88
+
64
89
  nil
65
90
  end
66
91
 
@@ -73,19 +98,47 @@ module Phlex::Elements
73
98
  end
74
99
 
75
100
  # @api private
76
- def register_void_element(method_name, tag: method_name.name.tr("_", "-"))
101
+ def register_void_element(method_name, tag: method_name.name.tr("_", "-"), deprecated: false)
102
+ if deprecated
103
+ deprecation = <<~RUBY
104
+ Kernel.warn "#{deprecated}"
105
+ RUBY
106
+ else
107
+ deprecation = ""
108
+ end
109
+
77
110
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
78
111
  # frozen_string_literal: true
79
112
 
80
113
  def #{method_name}(**attributes)
81
- target = @_context.target
114
+ #{deprecation}
115
+ context = @_context
116
+ buffer = context.buffer
117
+ fragment = context.fragments
118
+
119
+ if fragment
120
+ return if fragment.length == 0 # we found all our fragments already
121
+
122
+ id = attributes[:id]
123
+
124
+ if !context.in_target_fragment
125
+ if fragment[id]
126
+ context.begin_target(id)
127
+ target_found = true
128
+ else
129
+ return nil
130
+ end
131
+ end
132
+ end
82
133
 
83
134
  if attributes.length > 0 # with attributes
84
- target << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << ">"
135
+ buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] || __attributes__(**attributes)) << ">"
85
136
  else # without attributes
86
- target << "<#{tag}>"
137
+ buffer << "<#{tag}>"
87
138
  end
88
139
 
140
+ context.end_target if target_found
141
+
89
142
  nil
90
143
  end
91
144