poetry-core 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/app/components/poetry/core/component.rb +56 -0
- data/app/components/poetry/core/concerns/options.rb +14 -1
- data/lib/active_model/type/html_safe_string.rb +34 -0
- data/lib/poetry/core/check/stable_identity.rb +8 -3
- data/lib/poetry/core/check.rb +14 -9
- data/lib/poetry/core/version.rb +1 -1
- data/lib/poetry/core.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be0bfa6a89f4e90c5ff4227fb55faae46a68c2d8b6a85fbe7b954fe1a859314b
|
|
4
|
+
data.tar.gz: 466897acf54b0ada006299af237b50c8d32fe5ee846c8cac5d4b29723e93d33a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8d10b2fce56d3217292ff638aec3f3680202278f28144ed5a2a91c6f92fd51d2ea288b3b67987a9edc27fc3cb9e1c20e9fab94f44a22596553c98abb8ca69a6
|
|
7
|
+
data.tar.gz: 75db75e6f640a176bf84d99d64f8c4e354fcc563c1f01892b83af1d217ebd7879989d841cb62a4adef7e0a091c870e6bf5689f1410e0322b84fee7c3dc07fb2f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.8] - 2026-09-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `ActiveModel::Type::HtmlSafeString`, the type behind every `option :name, :string` declaration: an html-safe value (a hint built with `link_to`, a translated `_html` key) stays html-safe where ActiveModel's String type copied it into a plain String, so a Field's `hint:` or `error:` renders the markup it was given; a plain String still escapes at render. The type is substituted inside the option DSL only, never registered, and reports `:string`, so the registry reads every declaration unchanged.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- A content block that returns a scalar (a number, a symbol, a boolean) while writing nothing to the buffer captures as nothing, so `poetry_badge { count }` rendered an empty badge without a word. A component that declares `requires_content` now raises in development and test naming the value and the fix (call `to_s` on it, or write it with `<%= %>`) and logs in production; every other component logs. A block that wrote to the buffer, returned a String or nil, or returned a collection (an empty loop's `each`) is untouched.
|
|
12
|
+
- `poetry check`'s stable-identity rules skip a pathless helper whose registry `helpers` entry declares `identity: false` (a table part helper stamps no poetry-minted id, so `key:` had nothing to stabilize and the warning was noise), and read a double-splat of an id-bearing hash (`**field.control_attributes`, `**attrs`) as identity: the id arrives through the splat.
|
|
13
|
+
|
|
3
14
|
## [0.1.7] - 2026-09-20
|
|
4
15
|
|
|
5
16
|
### Changed
|
|
@@ -382,7 +382,63 @@ module Poetry
|
|
|
382
382
|
"(#{self.class.required_content})"
|
|
383
383
|
end
|
|
384
384
|
|
|
385
|
+
# ViewComponent's render_in, with the content block wrapped once so
|
|
386
|
+
# #content can read what the block RETURNED - capture keeps only what
|
|
387
|
+
# the block wrote to the buffer, and a scalar return vanishes there.
|
|
388
|
+
#
|
|
389
|
+
# @param view_context [ActionView::Base] the rendering view
|
|
390
|
+
# @return [ActiveSupport::SafeBuffer] the rendered component
|
|
391
|
+
def render_in(view_context, **)
|
|
392
|
+
@content_guarded = false
|
|
393
|
+
remove_instance_variable(:@content_return) if defined?(@content_return)
|
|
394
|
+
return super unless block_given?
|
|
395
|
+
|
|
396
|
+
recorded = proc { |*args| @content_return = yield(*args) }
|
|
397
|
+
super(view_context, **, &recorded)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# The captured content block, guarded once per render: a block that
|
|
401
|
+
# returned a scalar (a number, a symbol, a boolean) while writing
|
|
402
|
+
# nothing to the buffer captures as nothing at all - `poetry_badge {
|
|
403
|
+
# count }` renders an empty badge - so a requires_content component
|
|
404
|
+
# raises in development and test naming the value and the fix, and
|
|
405
|
+
# logs everywhere else; a component without the declaration logs. A
|
|
406
|
+
# block that wrote to the buffer, returned a String, returned nil,
|
|
407
|
+
# returned a collection (an empty loop's each) or composed slots
|
|
408
|
+
# (its last call a with_* setter, which returns the slot or the
|
|
409
|
+
# component) is fine.
|
|
410
|
+
#
|
|
411
|
+
# @return [ActiveSupport::SafeBuffer, nil] the captured content
|
|
412
|
+
def content
|
|
413
|
+
captured = super
|
|
414
|
+
guard_content_return(captured) unless @content_guarded
|
|
415
|
+
captured
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# The content-return guard behind #content.
|
|
419
|
+
#
|
|
420
|
+
# @param captured [ActiveSupport::SafeBuffer, nil] what capture kept
|
|
421
|
+
# @return [void]
|
|
422
|
+
# @raise [ArgumentError] a scalar return on a requires_content
|
|
423
|
+
# component, in development and test
|
|
424
|
+
def guard_content_return(captured)
|
|
425
|
+
@content_guarded = true
|
|
426
|
+
return unless defined?(@content_return)
|
|
427
|
+
|
|
428
|
+
value = @content_return
|
|
429
|
+
return if value.nil? || value.is_a?(::String) || value.respond_to?(:each) || captured.present?
|
|
430
|
+
return if value.is_a?(ViewComponent::Base) || value.is_a?(ViewComponent::Slot)
|
|
431
|
+
|
|
432
|
+
message = "#{passthrough_owner} content block returned #{value.inspect.truncate(60)} (#{value.class}), " \
|
|
433
|
+
"which captures as nothing - return a String (call to_s on it) or write it with <%= %>"
|
|
434
|
+
raise ArgumentError, message if strict_passthrough? && self.class.required_content
|
|
435
|
+
|
|
436
|
+
Rails.logger&.warn("poetry: #{message}") if defined?(Rails) && Rails.respond_to?(:logger)
|
|
437
|
+
end
|
|
438
|
+
private :guard_content_return
|
|
439
|
+
|
|
385
440
|
# Indicates whether this component instance is persisted.
|
|
441
|
+
|
|
386
442
|
# Always returns false as components are not persisted entities.
|
|
387
443
|
#
|
|
388
444
|
# @return [Boolean] always returns false
|
|
@@ -90,8 +90,9 @@ module Poetry
|
|
|
90
90
|
required, default_value = extract_declared_defaults(options)
|
|
91
91
|
register_option_format(name, options.delete(:format))
|
|
92
92
|
|
|
93
|
-
attribute(name, type, **options)
|
|
93
|
+
attribute(name, option_cast_type(type), **options)
|
|
94
94
|
add_option_validations(name, type, required)
|
|
95
|
+
|
|
95
96
|
setup_declared_tracking(:option, name, default_value)
|
|
96
97
|
define_type_getter(name, type)
|
|
97
98
|
end
|
|
@@ -200,6 +201,18 @@ module Poetry
|
|
|
200
201
|
validates name, presence: true if required
|
|
201
202
|
end
|
|
202
203
|
|
|
204
|
+
# The ActiveModel type an option declaration casts with. A :string
|
|
205
|
+
# option casts with ActiveModel::Type::HtmlSafeString, which keeps
|
|
206
|
+
# an html-safe value (a link, a translated `_html` key) html-safe
|
|
207
|
+
# where the String type copied it into a plain String; every other
|
|
208
|
+
# type is looked up by name as ActiveModel does.
|
|
209
|
+
#
|
|
210
|
+
# @param type [Symbol] the declared type
|
|
211
|
+
# @return [Symbol, ActiveModel::Type::Value] what `attribute` receives
|
|
212
|
+
def option_cast_type(type)
|
|
213
|
+
type == :string ? ActiveModel::Type::HtmlSafeString.new : type
|
|
214
|
+
end
|
|
215
|
+
|
|
203
216
|
# Defines a singleton method to access the type for this attribute.
|
|
204
217
|
#
|
|
205
218
|
# @param name [Symbol] the attribute name
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model/type"
|
|
4
|
+
|
|
5
|
+
module ActiveModel
|
|
6
|
+
module Type
|
|
7
|
+
# The string type behind every `option :name, :string` declaration:
|
|
8
|
+
# ActiveModel's String type copies each value into a fresh String,
|
|
9
|
+
# which drops the html_safe mark a SafeBuffer carries, so a caller's
|
|
10
|
+
# `hint: link_to(...)` or `t(".hint_html")` arrived escaped. This
|
|
11
|
+
# type keeps an html-safe value as it is and casts everything else
|
|
12
|
+
# exactly as the String type does; a plain String still escapes at
|
|
13
|
+
# render, because nothing here marks anything safe.
|
|
14
|
+
#
|
|
15
|
+
# Substituted inside the option DSL only - never registered, so
|
|
16
|
+
# `attribute :x, :string` in a host model keeps Rails' own type - and
|
|
17
|
+
# it reports :string, so the registry and every introspection surface
|
|
18
|
+
# read the declaration unchanged.
|
|
19
|
+
class HtmlSafeString < ActiveModel::Type::String
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# An html-safe String passes through untouched; everything else casts
|
|
23
|
+
# as a String does.
|
|
24
|
+
#
|
|
25
|
+
# @param value [Object] the raw option value
|
|
26
|
+
# @return [::String]
|
|
27
|
+
def cast_value(value)
|
|
28
|
+
return value if value.is_a?(::String) && value.html_safe?
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -24,8 +24,12 @@ module Poetry
|
|
|
24
24
|
#
|
|
25
25
|
# A component whose registry entry declares "identity" => false
|
|
26
26
|
# renders no poetry-minted id - key:/id: would have nothing to
|
|
27
|
-
# stabilize - so both rules skip it
|
|
28
|
-
#
|
|
27
|
+
# stabilize - so both rules skip it (a pathless helper declares the
|
|
28
|
+
# same in the registry helpers section). The registry derivation
|
|
29
|
+
# owns that fact per gem; legacy entries without the key keep
|
|
30
|
+
# warning. A double-splat of an id-bearing hash (`**field.control_attributes`,
|
|
31
|
+
# `**attrs`) counts as identity too: the id arrives through the splat,
|
|
32
|
+
# and the scan reads the name, not the hash.
|
|
29
33
|
#
|
|
30
34
|
# @api private
|
|
31
35
|
class StableIdentity
|
|
@@ -36,7 +40,8 @@ module Poetry
|
|
|
36
40
|
CACHE_OPENER = /\A\s*cache[\s(]/
|
|
37
41
|
LOOP_OPENER =
|
|
38
42
|
/\.(?:each|each_with_index|each_with_object|each_slice|map|collect|flat_map)\b.*\bdo\b|\Afor\s.+\sin\s/
|
|
39
|
-
IDENTITY_ARG =
|
|
43
|
+
IDENTITY_ARG =
|
|
44
|
+
/(?:\bkey:|\bid:|["']key["']\s*=>|["']id["']\s*=>|\*\*[\w.@]*(?:control_attributes|attributes|attrs)\b)/
|
|
40
45
|
|
|
41
46
|
# A stable-identity linter over the catalog.
|
|
42
47
|
def initialize(catalog)
|
data/lib/poetry/core/check.rb
CHANGED
|
@@ -197,12 +197,16 @@ module Poetry
|
|
|
197
197
|
# that yields its slot builder.
|
|
198
198
|
def helper_yields?(helper) = !@helper_entries.dig(helper, "yields").nil?
|
|
199
199
|
|
|
200
|
-
# A helper whose component entry declares "identity" => false
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
|
|
200
|
+
# A helper whose component entry declares "identity" => false, or
|
|
201
|
+
# a pathless helper whose registry helpers-section entry declares
|
|
202
|
+
# it (a table part helper stamps no poetry-minted id): its rendered
|
|
203
|
+
# output carries no random id, so key:/id: have nothing to
|
|
204
|
+
# stabilize and the stable-identity rules skip it. Legacy entries
|
|
205
|
+
# (no key) read as minting - their warnings stand.
|
|
206
|
+
def identity_free?(helper)
|
|
207
|
+
@components.dig(@path_by_helper[helper], "identity") == false ||
|
|
208
|
+
@helper_entries.dig(helper, "identity") == false
|
|
209
|
+
end
|
|
206
210
|
|
|
207
211
|
# The element-level wiring projection: which elements of
|
|
208
212
|
# a component carry which controllers, values, actions, and
|
|
@@ -1373,9 +1377,10 @@ module Poetry
|
|
|
1373
1377
|
# A click target that is not a button: an inert element carrying
|
|
1374
1378
|
# onclick or wearing role=button. A real <button> brings focus,
|
|
1375
1379
|
# keyboard activation, and the role for free, which is the first
|
|
1376
|
-
# rule of ARIA. Anchors stay out (a
|
|
1377
|
-
#
|
|
1378
|
-
#
|
|
1380
|
+
# rule of ARIA. Anchors stay out (a link is a link - the Button's
|
|
1381
|
+
# anchor form renders no role, and an author who adds one has
|
|
1382
|
+
# chosen it), and so does a Stimulus click on a bare div - a
|
|
1383
|
+
# backdrop or a row is a legitimate click target without a role.
|
|
1379
1384
|
def fake_button_findings(node)
|
|
1380
1385
|
tag = open_tag_name(node)
|
|
1381
1386
|
attributes = node.child_nodes.compact.grep(Herb::AST::HTMLAttributeNode)
|
data/lib/poetry/core/version.rb
CHANGED
data/lib/poetry/core.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "zeitwerk"
|
|
|
5
5
|
require "active_support/concern"
|
|
6
6
|
require "active_model/type/symbol"
|
|
7
7
|
require "active_model/type/list"
|
|
8
|
+
require "active_model/type/html_safe_string"
|
|
8
9
|
|
|
9
10
|
ActiveModel::Type.register(:symbol, ActiveModel::Type::Symbol)
|
|
10
11
|
ActiveModel::Type.register(:list, ActiveModel::Type::List)
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: poetry-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Solt
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -180,6 +180,7 @@ files:
|
|
|
180
180
|
- config/importmap.rb
|
|
181
181
|
- config/state_vocabulary.json
|
|
182
182
|
- eslint.config.mjs
|
|
183
|
+
- lib/active_model/type/html_safe_string.rb
|
|
183
184
|
- lib/active_model/type/list.rb
|
|
184
185
|
- lib/active_model/type/symbol.rb
|
|
185
186
|
- lib/poetry-core.rb
|