phlex 1.6.1 → 1.8.0
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.
Potentially problematic release.
This version of phlex might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +32 -0
- data/Gemfile +1 -0
- data/README.md +3 -43
- data/lib/phlex/black_hole.rb +1 -0
- data/lib/phlex/callable.rb +1 -0
- data/lib/phlex/context.rb +1 -0
- data/lib/phlex/deferred_render.rb +24 -0
- data/lib/phlex/element_clobbering_guard.rb +7 -8
- data/lib/phlex/elements.rb +38 -11
- data/lib/phlex/helpers.rb +21 -4
- data/lib/phlex/html/standard_elements.rb +193 -103
- data/lib/phlex/html/void_elements.rb +13 -12
- data/lib/phlex/html.rb +4 -6
- data/lib/phlex/overrides/symbol/name.rb +1 -0
- data/lib/phlex/sgml.rb +175 -64
- data/lib/phlex/svg/standard_elements.rb +128 -64
- data/lib/phlex/svg.rb +0 -4
- data/lib/phlex/unbuffered.rb +1 -0
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +19 -6
- metadata +9 -7
| @@ -1,76 +1,77 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            # Void HTML elements don't accept content and never have a closing tag.
         | 
| 3 4 | 
             
            module Phlex::HTML::VoidElements
         | 
| 4 5 | 
             
            	extend Phlex::Elements
         | 
| 5 6 |  | 
| 6 7 | 
             
            	# @!method area(**attributes, &content)
         | 
| 7 | 
            -
            	# 	Outputs an  | 
| 8 | 
            +
            	# 	Outputs an `<area>` tag.
         | 
| 8 9 | 
             
            	# 	@return [nil]
         | 
| 9 10 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/area
         | 
| 10 11 | 
             
            	register_void_element :area, tag: "area"
         | 
| 11 12 |  | 
| 12 13 | 
             
            	# @!method br(**attributes, &content)
         | 
| 13 | 
            -
            	# 	Outputs a  | 
| 14 | 
            +
            	# 	Outputs a `<br>` tag.
         | 
| 14 15 | 
             
            	# 	@return [nil]
         | 
| 15 16 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/br
         | 
| 16 17 | 
             
            	register_void_element :br, tag: "br"
         | 
| 17 18 |  | 
| 18 19 | 
             
            	# @!method embed(**attributes, &content)
         | 
| 19 | 
            -
            	# 	Outputs an  | 
| 20 | 
            +
            	# 	Outputs an `<embed>` tag.
         | 
| 20 21 | 
             
            	# 	@return [nil]
         | 
| 21 22 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/embed
         | 
| 22 23 | 
             
            	register_void_element :embed, tag: "embed"
         | 
| 23 24 |  | 
| 24 25 | 
             
            	# @!method hr(**attributes, &content)
         | 
| 25 | 
            -
            	# 	Outputs  | 
| 26 | 
            +
            	# 	Outputs an `<hr>` tag.
         | 
| 26 27 | 
             
            	# 	@return [nil]
         | 
| 27 28 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/hr
         | 
| 28 29 | 
             
            	register_void_element :hr, tag: "hr"
         | 
| 29 30 |  | 
| 30 31 | 
             
            	# @!method img(**attributes, &content)
         | 
| 31 | 
            -
            	# 	Outputs an  | 
| 32 | 
            +
            	# 	Outputs an `<img>` tag.
         | 
| 32 33 | 
             
            	# 	@return [nil]
         | 
| 33 34 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/img
         | 
| 34 35 | 
             
            	register_void_element :img, tag: "img"
         | 
| 35 36 |  | 
| 36 37 | 
             
            	# @!method input(**attributes, &content)
         | 
| 37 | 
            -
            	# 	Outputs an  | 
| 38 | 
            +
            	# 	Outputs an `<input>` tag.
         | 
| 38 39 | 
             
            	# 	@return [nil]
         | 
| 39 40 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/input
         | 
| 40 41 | 
             
            	register_void_element :input, tag: "input"
         | 
| 41 42 |  | 
| 42 43 | 
             
            	# @!method link(**attributes, &content)
         | 
| 43 | 
            -
            	# 	Outputs a  | 
| 44 | 
            +
            	# 	Outputs a `<link>` tag.
         | 
| 44 45 | 
             
            	# 	@return [nil]
         | 
| 45 46 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/link
         | 
| 46 47 | 
             
            	register_void_element :link, tag: "link"
         | 
| 47 48 |  | 
| 48 49 | 
             
            	# @!method meta(**attributes, &content)
         | 
| 49 | 
            -
            	# 	Outputs a  | 
| 50 | 
            +
            	# 	Outputs a `<meta>` tag.
         | 
| 50 51 | 
             
            	# 	@return [nil]
         | 
| 51 52 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/meta
         | 
| 52 53 | 
             
            	register_void_element :meta, tag: "meta"
         | 
| 53 54 |  | 
| 54 55 | 
             
            	# @!method param(**attributes, &content)
         | 
| 55 | 
            -
            	# 	Outputs a  | 
| 56 | 
            +
            	# 	Outputs a `<param>` tag.
         | 
| 56 57 | 
             
            	# 	@return [nil]
         | 
| 57 58 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/param
         | 
| 58 59 | 
             
            	register_void_element :param, tag: "param"
         | 
| 59 60 |  | 
| 60 61 | 
             
            	# @!method source(**attributes, &content)
         | 
| 61 | 
            -
            	# 	Outputs a  | 
| 62 | 
            +
            	# 	Outputs a `<source>` tag.
         | 
| 62 63 | 
             
            	# 	@return [nil]
         | 
| 63 64 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/source
         | 
| 64 65 | 
             
            	register_void_element :source, tag: "source"
         | 
| 65 66 |  | 
| 66 67 | 
             
            	# @!method track(**attributes, &content)
         | 
| 67 | 
            -
            	# 	Outputs a  | 
| 68 | 
            +
            	# 	Outputs a `<track>` tag.
         | 
| 68 69 | 
             
            	# 	@return [nil]
         | 
| 69 70 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/track
         | 
| 70 71 | 
             
            	register_void_element :track, tag: "track"
         | 
| 71 72 |  | 
| 72 73 | 
             
            	# @!method col(**attributes, &content)
         | 
| 73 | 
            -
            	# 	Outputs a  | 
| 74 | 
            +
            	# 	Outputs a `<col>` tag.
         | 
| 74 75 | 
             
            	# 	@return [nil]
         | 
| 75 76 | 
             
            	# 	@see https://developer.mozilla.org/docs/Web/HTML/Element/col
         | 
| 76 77 | 
             
            	register_void_element :col, tag: "col"
         | 
    
        data/lib/phlex/html.rb
    CHANGED
    
    | @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Phlex
         | 
| 4 | 
            +
            	# @abstract Subclass and define {#template} to create an HTML component class.
         | 
| 4 5 | 
             
            	class HTML < SGML
         | 
| 5 6 | 
             
            		# A list of HTML attributes that have the potential to execute unsafe JavaScript.
         | 
| 6 7 | 
             
            		EVENT_ATTRIBUTES = %w[onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmessage onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onstalled onstorage onsubmit onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel].to_h { [_1, true] }.freeze
         | 
| @@ -29,12 +30,9 @@ module Phlex | |
| 29 30 | 
             
            			nil
         | 
| 30 31 | 
             
            		end
         | 
| 31 32 |  | 
| 32 | 
            -
            		#  | 
| 33 | 
            -
            		 | 
| 34 | 
            -
             | 
| 35 | 
            -
            			plain(...)
         | 
| 36 | 
            -
            		end
         | 
| 37 | 
            -
             | 
| 33 | 
            +
            		# Outputs an `<svg>` tag
         | 
| 34 | 
            +
            		# @return [nil]
         | 
| 35 | 
            +
            		# @see https://developer.mozilla.org/docs/Web/SVG/Element/svg
         | 
| 38 36 | 
             
            		def svg(...)
         | 
| 39 37 | 
             
            			super do
         | 
| 40 38 | 
             
            				render Phlex::SVG.new do |svg|
         | 
    
        data/lib/phlex/sgml.rb
    CHANGED
    
    | @@ -5,15 +5,16 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0") | |
| 5 5 | 
             
            end
         | 
| 6 6 |  | 
| 7 7 | 
             
            module Phlex
         | 
| 8 | 
            +
            	# **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
         | 
| 8 9 | 
             
            	class SGML
         | 
| 9 10 | 
             
            		class << self
         | 
| 10 | 
            -
            			# Render the view to a String. Arguments are delegated to  | 
| 11 | 
            +
            			# Render the view to a String. Arguments are delegated to {.new}.
         | 
| 11 12 | 
             
            			def call(...)
         | 
| 12 13 | 
             
            				new(...).call
         | 
| 13 14 | 
             
            			end
         | 
| 14 15 |  | 
| 15 16 | 
             
            			# Create a new instance of the component.
         | 
| 16 | 
            -
            			# @note The block will not be delegated  | 
| 17 | 
            +
            			# @note The block will not be delegated {#initialize}. Instead, it will be sent to {#template} when rendering.
         | 
| 17 18 | 
             
            			def new(*args, **kwargs, &block)
         | 
| 18 19 | 
             
            				if block
         | 
| 19 20 | 
             
            					object = super(*args, **kwargs, &nil)
         | 
| @@ -42,22 +43,66 @@ module Phlex | |
| 42 43 | 
             
            			end
         | 
| 43 44 | 
             
            		end
         | 
| 44 45 |  | 
| 46 | 
            +
            		# @!method initialize
         | 
| 47 | 
            +
            		# @abstract Override to define an initializer for your component.
         | 
| 48 | 
            +
            		# @note Your initializer will not receive a block passed to {.new}. Instead, this block will be sent to {#template} when rendering.
         | 
| 49 | 
            +
            		# @example
         | 
| 50 | 
            +
            		# 	def initialize(articles:)
         | 
| 51 | 
            +
            		# 		@articles = articles
         | 
| 52 | 
            +
            		# 	end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            		# @abstract Override to define a template for your component.
         | 
| 55 | 
            +
            		# @example
         | 
| 56 | 
            +
            		# 	def template
         | 
| 57 | 
            +
            		# 		h1 { "👋 Hello World!" }
         | 
| 58 | 
            +
            		# 	end
         | 
| 59 | 
            +
            		# @example Your template may yield a content block.
         | 
| 60 | 
            +
            		# 	def template
         | 
| 61 | 
            +
            		# 		main {
         | 
| 62 | 
            +
            		# 			h1 { "Hello World" }
         | 
| 63 | 
            +
            		# 			yield
         | 
| 64 | 
            +
            		# 		}
         | 
| 65 | 
            +
            		# 	end
         | 
| 66 | 
            +
            		# @example Alternatively, you can delegate the content block to an element.
         | 
| 67 | 
            +
            		# 	def template(&block)
         | 
| 68 | 
            +
            		# 		article(class: "card", &block)
         | 
| 69 | 
            +
            		# 	end
         | 
| 70 | 
            +
            		def template
         | 
| 71 | 
            +
            			yield
         | 
| 72 | 
            +
            		end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
            		# @api private
         | 
| 75 | 
            +
            		def await(task)
         | 
| 76 | 
            +
            			if task.is_a?(Concurrent::IVar)
         | 
| 77 | 
            +
            				flush if task.pending?
         | 
| 78 | 
            +
             | 
| 79 | 
            +
            				task.wait.value
         | 
| 80 | 
            +
            			elsif defined?(Async::Task) && task.is_a?(Async::Task)
         | 
| 81 | 
            +
            				flush if task.running?
         | 
| 82 | 
            +
             | 
| 83 | 
            +
            				task.wait
         | 
| 84 | 
            +
            			else
         | 
| 85 | 
            +
            				raise ArgumentError, "Expected an asynchronous task / promise."
         | 
| 86 | 
            +
            			end
         | 
| 87 | 
            +
            		end
         | 
| 88 | 
            +
             | 
| 45 89 | 
             
            		# Renders the view and returns the buffer. The default buffer is a mutable String.
         | 
| 46 | 
            -
            		def call(buffer =  | 
| 90 | 
            +
            		def call(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
         | 
| 47 91 | 
             
            			__final_call__(buffer, context: context, view_context: view_context, parent: parent, &block).tap do
         | 
| 48 92 | 
             
            				self.class.rendered_at_least_once!
         | 
| 49 93 | 
             
            			end
         | 
| 50 94 | 
             
            		end
         | 
| 51 95 |  | 
| 52 96 | 
             
            		# @api private
         | 
| 53 | 
            -
            		def __final_call__(buffer =  | 
| 97 | 
            +
            		def __final_call__(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
         | 
| 98 | 
            +
            			@_buffer = buffer
         | 
| 54 99 | 
             
            			@_context = context
         | 
| 55 100 | 
             
            			@_view_context = view_context
         | 
| 56 101 | 
             
            			@_parent = parent
         | 
| 57 102 |  | 
| 58 103 | 
             
            			block ||= @_content_block
         | 
| 59 104 |  | 
| 60 | 
            -
            			return  | 
| 105 | 
            +
            			return unless render?
         | 
| 61 106 |  | 
| 62 107 | 
             
            			around_template do
         | 
| 63 108 | 
             
            				if block
         | 
| @@ -78,47 +123,16 @@ module Phlex | |
| 78 123 | 
             
            				end
         | 
| 79 124 | 
             
            			end
         | 
| 80 125 |  | 
| 81 | 
            -
            			buffer  | 
| 82 | 
            -
            		end
         | 
| 83 | 
            -
             | 
| 84 | 
            -
            		# Render another view
         | 
| 85 | 
            -
            		# @param renderable [Phlex::SGML]
         | 
| 86 | 
            -
            		# @return [nil]
         | 
| 87 | 
            -
            		def render(renderable, &block)
         | 
| 88 | 
            -
            			case renderable
         | 
| 89 | 
            -
            			when Phlex::SGML
         | 
| 90 | 
            -
            				renderable.call(context: @_context, view_context: @_view_context, parent: self, &block)
         | 
| 91 | 
            -
            			when Class
         | 
| 92 | 
            -
            				if renderable < Phlex::SGML
         | 
| 93 | 
            -
            					renderable.new.call(context: @_context, view_context: @_view_context, parent: self, &block)
         | 
| 94 | 
            -
            				end
         | 
| 95 | 
            -
            			when Enumerable
         | 
| 96 | 
            -
            				renderable.each { |r| render(r, &block) }
         | 
| 97 | 
            -
            			when Proc
         | 
| 98 | 
            -
            				yield_content(&renderable)
         | 
| 99 | 
            -
            			else
         | 
| 100 | 
            -
            				raise ArgumentError, "You can't render a #{renderable}."
         | 
| 101 | 
            -
            			end
         | 
| 102 | 
            -
             | 
| 103 | 
            -
            			nil
         | 
| 126 | 
            +
            			buffer << context.target unless parent
         | 
| 104 127 | 
             
            		end
         | 
| 105 128 |  | 
| 106 129 | 
             
            		# Output text content. The text will be HTML-escaped.
         | 
| 130 | 
            +
            		# @param content [String, Symbol, Integer, void] the content to be output on the buffer. Strings, Symbols, and Integers are handled by `plain` directly, but any object can be handled by overriding `format_object`
         | 
| 107 131 | 
             
            		# @return [nil]
         | 
| 132 | 
            +
            		# @see #format_object
         | 
| 108 133 | 
             
            		def plain(content)
         | 
| 109 | 
            -
            			 | 
| 110 | 
            -
             | 
| 111 | 
            -
            				@_context.target << ERB::Escape.html_escape(content)
         | 
| 112 | 
            -
            			when Symbol
         | 
| 113 | 
            -
            				@_context.target << ERB::Escape.html_escape(content.name)
         | 
| 114 | 
            -
            			when Integer
         | 
| 115 | 
            -
            				@_context.target << ERB::Escape.html_escape(content.to_s)
         | 
| 116 | 
            -
            			when nil
         | 
| 117 | 
            -
            				nil
         | 
| 118 | 
            -
            			else
         | 
| 119 | 
            -
            				if (formatted_object = format_object(content))
         | 
| 120 | 
            -
            					@_context.target << ERB::Escape.html_escape(formatted_object)
         | 
| 121 | 
            -
            				end
         | 
| 134 | 
            +
            			unless __text__(content)
         | 
| 135 | 
            +
            				raise ArgumentError, "You've passed an object to plain that is not handled by format_object. See https://rubydoc.info/gems/phlex/Phlex/SGML#format_object-instance_method for more information"
         | 
| 122 136 | 
             
            			end
         | 
| 123 137 |  | 
| 124 138 | 
             
            			nil
         | 
| @@ -126,6 +140,7 @@ module Phlex | |
| 126 140 |  | 
| 127 141 | 
             
            		# Output a whitespace character. This is useful for getting inline elements to wrap. If you pass a block, a whitespace will be output before and after yielding the block.
         | 
| 128 142 | 
             
            		# @return [nil]
         | 
| 143 | 
            +
            		# @yield If a block is given, it yields the block with no arguments.
         | 
| 129 144 | 
             
            		def whitespace
         | 
| 130 145 | 
             
            			target = @_context.target
         | 
| 131 146 |  | 
| @@ -170,10 +185,59 @@ module Phlex | |
| 170 185 | 
             
            			@_context.with_target(+"") { yield_content(&block) }
         | 
| 171 186 | 
             
            		end
         | 
| 172 187 |  | 
| 173 | 
            -
            		 | 
| 188 | 
            +
            		private
         | 
| 189 | 
            +
             | 
| 190 | 
            +
            		# @api private
         | 
| 191 | 
            +
            		def flush
         | 
| 192 | 
            +
            			target = @_context.target
         | 
| 193 | 
            +
            			@_buffer << target.dup
         | 
| 194 | 
            +
            			target.clear
         | 
| 195 | 
            +
            		end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
            		# Render another component, block or enumerable
         | 
| 198 | 
            +
            		# @return [nil]
         | 
| 199 | 
            +
            		# @overload render(component, &block)
         | 
| 200 | 
            +
            		# 	Renders the component.
         | 
| 201 | 
            +
            		# 	@param component [Phlex::SGML]
         | 
| 202 | 
            +
            		# @overload render(component_class, &block)
         | 
| 203 | 
            +
            		# 	Renders a new instance of the component class. This is useful for component classes that take no arguments.
         | 
| 204 | 
            +
            		# 	@param component_class [Class<Phlex::SGML>]
         | 
| 205 | 
            +
            		# @overload render(proc)
         | 
| 206 | 
            +
            		# 	Renders the proc with {#yield_content}.
         | 
| 207 | 
            +
            		# 	@param proc [Proc]
         | 
| 208 | 
            +
            		# @overload render(enumerable)
         | 
| 209 | 
            +
            		# 	Renders each item of the enumerable.
         | 
| 210 | 
            +
            		# 	@param enumerable [Enumerable]
         | 
| 211 | 
            +
            		# 	@example
         | 
| 212 | 
            +
            		# 		render @items
         | 
| 213 | 
            +
            		def render(renderable, &block)
         | 
| 214 | 
            +
            			case renderable
         | 
| 215 | 
            +
            			when Phlex::SGML
         | 
| 216 | 
            +
            				renderable.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
         | 
| 217 | 
            +
            			when Class
         | 
| 218 | 
            +
            				if renderable < Phlex::SGML
         | 
| 219 | 
            +
            					renderable.new.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
         | 
| 220 | 
            +
            				end
         | 
| 221 | 
            +
            			when Enumerable
         | 
| 222 | 
            +
            				renderable.each { |r| render(r, &block) }
         | 
| 223 | 
            +
            			when Proc
         | 
| 224 | 
            +
            				if renderable.arity == 0
         | 
| 225 | 
            +
            					yield_content_with_no_args(&renderable)
         | 
| 226 | 
            +
            				else
         | 
| 227 | 
            +
            					yield_content(&renderable)
         | 
| 228 | 
            +
            				end
         | 
| 229 | 
            +
            			else
         | 
| 230 | 
            +
            				raise ArgumentError, "You can't render a #{renderable}."
         | 
| 231 | 
            +
            			end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
            			nil
         | 
| 234 | 
            +
            		end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
            		# Like {#capture} but the output is vanished into a BlackHole buffer.
         | 
| 174 237 | 
             
            		# Because the BlackHole does nothing with the output, this should be faster.
         | 
| 175 238 | 
             
            		# @return [nil]
         | 
| 176 | 
            -
            		 | 
| 239 | 
            +
            		# @api private
         | 
| 240 | 
            +
            		def __vanish__(*args)
         | 
| 177 241 | 
             
            			return unless block_given?
         | 
| 178 242 |  | 
| 179 243 | 
             
            			@_context.with_target(BlackHole) { yield(*args) }
         | 
| @@ -181,24 +245,26 @@ module Phlex | |
| 181 245 | 
             
            			nil
         | 
| 182 246 | 
             
            		end
         | 
| 183 247 |  | 
| 184 | 
            -
            		#  | 
| 185 | 
            -
            		# @ | 
| 186 | 
            -
            		 | 
| 248 | 
            +
            		# Determines if the component should render. By default, it returns `true`.
         | 
| 249 | 
            +
            		# @abstract Override to define your own predicate to prevent rendering.
         | 
| 250 | 
            +
            		# @return [Boolean]
         | 
| 251 | 
            +
            		def render?
         | 
| 187 252 | 
             
            			true
         | 
| 188 253 | 
             
            		end
         | 
| 189 254 |  | 
| 190 255 | 
             
            		# Format the object for output
         | 
| 256 | 
            +
            		# @abstract Override to define your own format handling for different object types. Please remember to call `super` in the case that the passed object doesn't match, so that object formatting can be added at different layers of the inheritance tree.
         | 
| 191 257 | 
             
            		# @return [String]
         | 
| 192 | 
            -
            		 | 
| 258 | 
            +
            		def format_object(object)
         | 
| 193 259 | 
             
            			case object
         | 
| 194 260 | 
             
            			when Float
         | 
| 195 261 | 
             
            				object.to_s
         | 
| 196 262 | 
             
            			end
         | 
| 197 263 | 
             
            		end
         | 
| 198 264 |  | 
| 199 | 
            -
            		# Override this method to hook in around a template render. You can do things before and after calling  | 
| 265 | 
            +
            		# @abstract Override this method to hook in around a template render. You can do things before and after calling `super` to render the template. You should always call `super` so that callbacks can be added at different layers of the inheritance tree.
         | 
| 200 266 | 
             
            		# @return [nil]
         | 
| 201 | 
            -
            		 | 
| 267 | 
            +
            		def around_template
         | 
| 202 268 | 
             
            			before_template
         | 
| 203 269 | 
             
            			yield
         | 
| 204 270 | 
             
            			after_template
         | 
| @@ -206,55 +272,94 @@ module Phlex | |
| 206 272 | 
             
            			nil
         | 
| 207 273 | 
             
            		end
         | 
| 208 274 |  | 
| 209 | 
            -
            		# Override this method to hook in right before a template is rendered. Please remember to call  | 
| 275 | 
            +
            		# @abstract Override this method to hook in right before a template is rendered. Please remember to call `super` so that callbacks can be added at different layers of the inheritance tree.
         | 
| 210 276 | 
             
            		# @return [nil]
         | 
| 211 | 
            -
            		 | 
| 277 | 
            +
            		def before_template
         | 
| 212 278 | 
             
            			nil
         | 
| 213 279 | 
             
            		end
         | 
| 214 280 |  | 
| 215 | 
            -
            		# Override this method to hook in right after a template is rendered. Please remember to call  | 
| 281 | 
            +
            		# @abstract Override this method to hook in right after a template is rendered. Please remember to call `super` so that callbacks can be added at different layers of the inheritance tree.
         | 
| 216 282 | 
             
            		# @return [nil]
         | 
| 217 | 
            -
            		 | 
| 283 | 
            +
            		def after_template
         | 
| 218 284 | 
             
            			nil
         | 
| 219 285 | 
             
            		end
         | 
| 220 286 |  | 
| 221 | 
            -
            		# Yields the block and checks if it buffered anything. If nothing was buffered, the return value is treated as text.
         | 
| 287 | 
            +
            		# Yields the block and checks if it buffered anything. If nothing was buffered, the return value is treated as text. The text is always HTML-escaped.
         | 
| 288 | 
            +
            		# @yieldparam component [self]
         | 
| 222 289 | 
             
            		# @return [nil]
         | 
| 223 | 
            -
            		 | 
| 290 | 
            +
            		def yield_content
         | 
| 224 291 | 
             
            			return unless block_given?
         | 
| 225 292 |  | 
| 226 293 | 
             
            			target = @_context.target
         | 
| 227 294 |  | 
| 228 295 | 
             
            			original_length = target.length
         | 
| 229 296 | 
             
            			content = yield(self)
         | 
| 230 | 
            -
            			 | 
| 297 | 
            +
            			__text__(content) if original_length == target.length
         | 
| 298 | 
            +
             | 
| 299 | 
            +
            			nil
         | 
| 300 | 
            +
            		end
         | 
| 301 | 
            +
             | 
| 302 | 
            +
            		# Same as {#yield_content} but yields no arguments.
         | 
| 303 | 
            +
            		# @yield Yields the block with no arguments.
         | 
| 304 | 
            +
            		def yield_content_with_no_args
         | 
| 305 | 
            +
            			return unless block_given?
         | 
| 306 | 
            +
             | 
| 307 | 
            +
            			target = @_context.target
         | 
| 308 | 
            +
             | 
| 309 | 
            +
            			original_length = target.length
         | 
| 310 | 
            +
            			content = yield
         | 
| 311 | 
            +
            			__text__(content) if original_length == target.length
         | 
| 231 312 |  | 
| 232 313 | 
             
            			nil
         | 
| 233 314 | 
             
            		end
         | 
| 234 315 |  | 
| 235 | 
            -
            		# Same as  | 
| 316 | 
            +
            		# Same as {#yield_content} but accepts a splat of arguments to yield. This is slightly slower than {#yield_content}.
         | 
| 317 | 
            +
            		# @yield [*args] Yields the given arguments.
         | 
| 236 318 | 
             
            		# @return [nil]
         | 
| 237 | 
            -
            		 | 
| 319 | 
            +
            		def yield_content_with_args(*args)
         | 
| 238 320 | 
             
            			return unless block_given?
         | 
| 239 321 |  | 
| 240 322 | 
             
            			target = @_context.target
         | 
| 241 323 |  | 
| 242 324 | 
             
            			original_length = target.length
         | 
| 243 325 | 
             
            			content = yield(*args)
         | 
| 244 | 
            -
            			 | 
| 326 | 
            +
            			__text__(content) if original_length == target.length
         | 
| 245 327 |  | 
| 246 328 | 
             
            			nil
         | 
| 247 329 | 
             
            		end
         | 
| 248 330 |  | 
| 331 | 
            +
            		# Performs the same task as the public method #plain, but does not raise an error if an unformattable object is passed
         | 
| 332 | 
            +
            		# @api private
         | 
| 333 | 
            +
            		def __text__(content)
         | 
| 334 | 
            +
            			case content
         | 
| 335 | 
            +
            			when String
         | 
| 336 | 
            +
            				@_context.target << ERB::Escape.html_escape(content)
         | 
| 337 | 
            +
            			when Symbol
         | 
| 338 | 
            +
            				@_context.target << ERB::Escape.html_escape(content.name)
         | 
| 339 | 
            +
            			when Integer
         | 
| 340 | 
            +
            				@_context.target << content.to_s
         | 
| 341 | 
            +
            			when nil
         | 
| 342 | 
            +
            				nil
         | 
| 343 | 
            +
            			else
         | 
| 344 | 
            +
            				if (formatted_object = format_object(content))
         | 
| 345 | 
            +
            					@_context.target << ERB::Escape.html_escape(formatted_object)
         | 
| 346 | 
            +
            				else
         | 
| 347 | 
            +
            					return false
         | 
| 348 | 
            +
            				end
         | 
| 349 | 
            +
            			end
         | 
| 350 | 
            +
             | 
| 351 | 
            +
            			true
         | 
| 352 | 
            +
            		end
         | 
| 353 | 
            +
             | 
| 249 354 | 
             
            		# @api private
         | 
| 250 | 
            -
            		 | 
| 355 | 
            +
            		def __attributes__(**attributes)
         | 
| 251 356 | 
             
            			__final_attributes__(**attributes).tap do |buffer|
         | 
| 252 357 | 
             
            				Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] = buffer.freeze
         | 
| 253 358 | 
             
            			end
         | 
| 254 359 | 
             
            		end
         | 
| 255 360 |  | 
| 256 361 | 
             
            		# @api private
         | 
| 257 | 
            -
            		 | 
| 362 | 
            +
            		def __final_attributes__(**attributes)
         | 
| 258 363 | 
             
            			if respond_to?(:process_attributes)
         | 
| 259 364 | 
             
            				attributes = process_attributes(**attributes)
         | 
| 260 365 | 
             
            			end
         | 
| @@ -274,14 +379,14 @@ module Phlex | |
| 274 379 | 
             
            		end
         | 
| 275 380 |  | 
| 276 381 | 
             
            		# @api private
         | 
| 277 | 
            -
            		 | 
| 382 | 
            +
            		def __build_attributes__(attributes, buffer:)
         | 
| 278 383 | 
             
            			attributes.each do |k, v|
         | 
| 279 384 | 
             
            				next unless v
         | 
| 280 385 |  | 
| 281 386 | 
             
            				name = case k
         | 
| 282 387 | 
             
            					when String then k
         | 
| 283 388 | 
             
            					when Symbol then k.name.tr("_", "-")
         | 
| 284 | 
            -
            					else  | 
| 389 | 
            +
            					else raise ArgumentError, "Attribute keys should be Strings or Symbols."
         | 
| 285 390 | 
             
            				end
         | 
| 286 391 |  | 
| 287 392 | 
             
            				# Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
         | 
| @@ -296,6 +401,8 @@ module Phlex | |
| 296 401 | 
             
            					buffer << " " << name << '="' << ERB::Escape.html_escape(v) << '"'
         | 
| 297 402 | 
             
            				when Symbol
         | 
| 298 403 | 
             
            					buffer << " " << name << '="' << ERB::Escape.html_escape(v.name) << '"'
         | 
| 404 | 
            +
            				when Integer, Float
         | 
| 405 | 
            +
            					buffer << " " << name << '="' << v.to_s << '"'
         | 
| 299 406 | 
             
            				when Hash
         | 
| 300 407 | 
             
            					__build_attributes__(
         | 
| 301 408 | 
             
            						v.transform_keys { |subkey|
         | 
| @@ -305,8 +412,12 @@ module Phlex | |
| 305 412 | 
             
            							end
         | 
| 306 413 | 
             
            						}, buffer: buffer
         | 
| 307 414 | 
             
            					)
         | 
| 415 | 
            +
            				when Array
         | 
| 416 | 
            +
            					buffer << " " << name << '="' << ERB::Escape.html_escape(v.compact.join(" ")) << '"'
         | 
| 417 | 
            +
            				when Set
         | 
| 418 | 
            +
            					buffer << " " << name << '="' << ERB::Escape.html_escape(v.to_a.compact.join(" ")) << '"'
         | 
| 308 419 | 
             
            				else
         | 
| 309 | 
            -
            					buffer << " " << name << '="' << ERB::Escape.html_escape(v. | 
| 420 | 
            +
            					buffer << " " << name << '="' << ERB::Escape.html_escape(v.to_str) << '"'
         | 
| 310 421 | 
             
            				end
         | 
| 311 422 | 
             
            			end
         | 
| 312 423 |  |