style_capsule 1.3.0 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0dc57ce596c8aec7767199f8c57b82db16a8f96b33ac06cba599febd89afa86a
4
- data.tar.gz: b4767a7eed2a864357027386452ea6a2da24577be3376388569c8ceb61bff937
3
+ metadata.gz: a44279976aff6cedb8ddc1a8bc4c84382378d73a11ebb7a17850626ecd612745
4
+ data.tar.gz: fee43b83895392f778b7c8b3600dbb5e75e52d660efc76d7939e762ef594e94d
5
5
  SHA512:
6
- metadata.gz: a666c80dc9ddc70179e9992bc4a1c0c9b58a295e21e0fc64441227529c4b401fdc7c7a703af5e87403ca407d0e482a5b97c73b1cc81d385a021cea2e1186c8d5
7
- data.tar.gz: 0d8523c1659c5467b6e0a1aa6fc1268f999f669772a2ff22b66b386ec5c6ed19d85a9b9dc2b915f744d691ad2d9ccaa2dd7bc33f5c132a3c7d1fe1df073dcd41
6
+ metadata.gz: a82489272c3ff1e9ac782e2129dc8133a2037101a9719f6c1a38404cec71dae33dab6f64ae93ff6926cd57bcd2456412f87dc61e0c61d04996b226a66466f411
7
+ data.tar.gz: 6c50c563aba3098121e4d9f2693572c6b66cbf6304fff198cafd337ac33da190af06a8469a2a6b59108c1392e2c4b844d13d0dd1cf56025faf5640bbbb1f5bdc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 2.0.0 (2026-07-03)
6
+
7
+ - BREAKING: split `StylesheetRegistry.register` into request-scoped `register` and boot-time `register_eager`
8
+ - Replace boot-time `StylesheetRegistry.register(...)` with `register_eager(...)`; keep `register` for render-time / `register_stylesheet` paths
9
+ - BREAKING: enable `StyleCapsule::HeadInjectionMiddleware` by default (`config.style_capsule.head_injection_middleware`); set to `false` to disable
10
+ - Fix render-time `register_stylesheet` missing from `<head>` on the same request when layouts render head before body
11
+ - Add `HeadInjectionMiddleware` to inject pending stylesheet tags before `</head>` after the response body is rendered
12
+ - Skips chunked responses and only buffers when pending request-scoped stylesheets exist; accepts any 2xx HTML response
13
+ - Dedupe eager manifest and request-scoped file paths by logical path when rendering `<head>` (request registration wins on option conflicts)
14
+ - Bound class-level component CSS cache (`MAX_CSS_CACHE_ENTRIES` = 256)
15
+ - Single-pass `<style>` extraction in ERB / standalone helpers (`StringScanner`)
16
+ - Reject parent path segments (`..`) in `AssetPath.validate_logical_path!`
17
+ - Add `StyleCapsule::StylesheetRegistry.inject_pending_head_stylesheets` for manual/testing use
18
+ - Extract shared Phlex / ViewComponent class DSL into `StyleCapsule::ComponentClassMethods`
19
+ - Fix class-level `scope_css` cache keys to include a CSS fingerprint (instance methods returning different styles per render)
20
+ - Fix ERB / standalone `Helper#scope_css` thread cache keys to include a CSS fingerprint
21
+ - Add `tag:` option to `StandaloneHelper#style_capsule` (aligned with the Rails helper)
22
+ - Fix `PhlexHelper#stylesheet_registry_tags` to always return a string when `safe` is unavailable
23
+ - Pass `component_class:` into `CssProcessor` from `ViewComponent` for consistent instrumentation
24
+ - Defer `input_size` in `Instrumentation.instrument_css_processing` until instrumentation runs
25
+ - Documentation: add [docs/non_rails_support.md](docs/non_rails_support.md); document late head injection, `register_eager`, streaming / Live caveats
26
+ - RBS: `StylesheetRegistry` no longer declared as a subclass of `ActiveSupport::CurrentAttributes` only
27
+
28
+ ## 1.4.0 (2025-11-26)
29
+
30
+ - Added unified `style_capsule` class method for configuring all StyleCapsule settings (namespace, cache strategy, CSS scoping, head rendering) in a single call
31
+ - Added automatic namespace fallback in `register_stylesheet` helper methods - when namespace is not specified, uses the component's configured namespace from `style_capsule`
32
+ - Removed deprecated `head_rendering!` method (use `style_capsule` instead)
33
+ - Removed deprecated `stylesheet_registrymap_tags` alias (use `stylesheet_registry_tags` instead)
34
+ - Refactored namespace configuration to use instance variables instead of constants for better inheritance behavior
35
+
3
36
  ## 1.3.0 (2025-11-26)
4
37
 
5
38
  - Added comprehensive instrumentation via `StyleCapsule::Instrumentation` using ActiveSupport::Notifications
@@ -29,7 +62,7 @@
29
62
  - Rails integration remains fully supported via Railtie
30
63
  - Added `StyleCapsule::StandaloneHelper` for non-Rails frameworks
31
64
  - `StylesheetRegistry` now works without `ActiveSupport::CurrentAttributes` using thread-local storage fallback
32
- - Renamed `stylesheet_registrymap_tags` to `stylesheet_registry_tags` (old name kept as deprecated alias)
65
+ - Renamed `stylesheet_registrymap_tags` to `stylesheet_registry_tags` (deprecated alias removed in 1.4.0)
33
66
  - Extracted CSS building logic from Rake tasks into `StyleCapsule::ComponentBuilder`
34
67
  - Fixed XSS vulnerability in `escape_html_attr` by using `CGI.escapeHTML` for proper HTML entity escaping
35
68
  - Optimized ActiveSupport require to avoid exception handling overhead in Rails apps
data/README.md CHANGED
@@ -1,15 +1,9 @@
1
1
  # style_capsule
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/style_capsule.svg?v=1.3.0)](https://badge.fury.io/rb/style_capsule) [![Test Status](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/style_capsule.rb/graph/badge.svg?token=2U6NXJOVVM)](https://codecov.io/gh/amkisko/style_capsule.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/style_capsule.svg?v=2.0.0)](https://badge.fury.io/rb/style_capsule) [![Test Status](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/style_capsule.rb/graph/badge.svg?token=2U6NXJOVVM)](https://codecov.io/gh/amkisko/style_capsule.rb)
4
4
 
5
5
  CSS scoping extension for Ruby components. Provides attribute-based style encapsulation for Phlex, ViewComponent, and ERB templates to prevent style leakage between components. Works with Rails and can be used standalone in other Ruby frameworks (Sinatra, Hanami, etc.) or plain Ruby scripts. Includes configurable caching strategies for optimal performance.
6
6
 
7
- Sponsored by [Kisko Labs](https://www.kiskolabs.com).
8
-
9
- <a href="https://www.kiskolabs.com">
10
- <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
11
- </a>
12
-
13
7
  ## Installation
14
8
 
15
9
  Add to your Gemfile:
@@ -88,6 +82,17 @@ end
88
82
  <% end %>
89
83
  ```
90
84
 
85
+ **With custom wrapper tag:**
86
+
87
+ ```erb
88
+ <%= style_capsule(tag: :section) do %>
89
+ <style>
90
+ .section { color: red; }
91
+ </style>
92
+ <div class="section">Content</div>
93
+ <% end %>
94
+ ```
95
+
91
96
  ## CSS Scoping Strategies
92
97
 
93
98
  StyleCapsule supports two CSS scoping strategies:
@@ -103,12 +108,21 @@ StyleCapsule supports two CSS scoping strategies:
103
108
 
104
109
  ### Configuration
105
110
 
106
- **Per-component:**
111
+ **Per-component (using `style_capsule` - recommended):**
112
+
113
+ ```ruby
114
+ class MyComponent < ApplicationComponent
115
+ include StyleCapsule::Component
116
+ style_capsule scoping_strategy: :nesting # Use CSS nesting
117
+ end
118
+ ```
119
+
120
+ **With custom wrapper tag:**
107
121
 
108
122
  ```ruby
109
123
  class MyComponent < ApplicationComponent
110
124
  include StyleCapsule::Component
111
- css_scoping_strategy :nesting # Use CSS nesting
125
+ style_capsule tag: :section # Use <section> instead of <div> for wrapper
112
126
  end
113
127
  ```
114
128
 
@@ -117,7 +131,7 @@ end
117
131
  ```ruby
118
132
  class ApplicationComponent < Phlex::HTML
119
133
  include StyleCapsule::Component
120
- css_scoping_strategy :nesting # Enable for all components
134
+ style_capsule scoping_strategy: :nesting # Enable for all components
121
135
  end
122
136
  ```
123
137
 
@@ -129,12 +143,12 @@ MyComponent.clear_css_cache
129
143
 
130
144
  ## Stylesheet Registry
131
145
 
132
- For better performance, register styles for head rendering instead of rendering `<style>` tags in the body:
146
+ For better performance, register styles for head rendering instead of rendering `<style>` tags in the body. Use the unified `style_capsule` method to configure all settings:
133
147
 
134
148
  ```ruby
135
149
  class MyComponent < ApplicationComponent
136
150
  include StyleCapsule::Component
137
- stylesheet_registry namespace: :admin # Optional namespace
151
+ style_capsule namespace: :admin # Configure namespace and enable head rendering
138
152
 
139
153
  def component_styles
140
154
  <<~CSS
@@ -144,12 +158,17 @@ class MyComponent < ApplicationComponent
144
158
  end
145
159
  ```
146
160
 
147
- With cache strategy:
161
+ With cache strategy and CSS scoping:
148
162
 
149
163
  ```ruby
150
164
  class MyComponent < ApplicationComponent
151
165
  include StyleCapsule::Component
152
- stylesheet_registry namespace: :admin, cache_strategy: :time, cache_ttl: 1.hour
166
+ style_capsule(
167
+ namespace: :admin,
168
+ cache_strategy: :time,
169
+ cache_ttl: 1.hour,
170
+ scoping_strategy: :nesting
171
+ )
153
172
 
154
173
  def component_styles
155
174
  <<~CSS
@@ -159,11 +178,10 @@ class MyComponent < ApplicationComponent
159
178
  end
160
179
  ```
161
180
 
162
- Then in your layout:
181
+ Then in your layout (render only the namespace you need):
163
182
 
164
183
  ```erb
165
184
  <head>
166
- <%= stylesheet_registry_tags %>
167
185
  <%= stylesheet_registry_tags(namespace: :admin) %>
168
186
  </head>
169
187
  ```
@@ -172,42 +190,73 @@ Or in Phlex (requires including `StyleCapsule::PhlexHelper`):
172
190
 
173
191
  ```ruby
174
192
  head do
175
- stylesheet_registry_tags
193
+ stylesheet_registry_tags(namespace: :admin)
176
194
  end
177
195
  ```
178
196
 
197
+ **Namespace Isolation:** Using namespaces prevents stylesheet leakage between different application contexts. For example, login pages can use `namespace: :login`, ActiveAdmin can use `namespace: :active_admin`, and user components can use `namespace: :user`. Each namespace is rendered separately, improving caching efficiency and preventing style conflicts.
198
+
179
199
  ### Registering Stylesheet Files
180
200
 
181
- You can also register external stylesheet files (not inline CSS) for head rendering:
201
+ You can also register external stylesheet files (not inline CSS) for head rendering. When a component has a configured namespace via `style_capsule`, you don't need to specify it every time:
182
202
 
183
203
  **In ERB:**
184
204
 
185
205
  ```erb
186
- <% register_stylesheet("stylesheets/user/order_select_component", "data-turbo-track": "reload") %>
206
+ <% register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload") %>
187
207
  <% register_stylesheet("stylesheets/admin/dashboard", namespace: :admin) %>
188
208
  ```
189
209
 
190
210
  **In Phlex (requires including `StyleCapsule::PhlexHelper`):**
191
211
 
192
212
  ```ruby
193
- def view_template
194
- register_stylesheet("stylesheets/user/order_select_component", "data-turbo-track": "reload")
195
- register_stylesheet("stylesheets/admin/dashboard", namespace: :admin)
196
- div { "Content" }
213
+ class UserComponent < ApplicationComponent
214
+ include StyleCapsule::Component
215
+ include StyleCapsule::PhlexHelper
216
+ style_capsule namespace: :user # Set default namespace
217
+
218
+ def view_template
219
+ # Namespace automatically uses :user from style_capsule
220
+ register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload")
221
+ # Can still override namespace if needed
222
+ register_stylesheet("stylesheets/shared/common", namespace: :shared)
223
+ div { "Content" }
224
+ end
197
225
  end
198
226
  ```
199
227
 
200
228
  **In ViewComponent (requires including `StyleCapsule::ViewComponentHelper`):**
201
229
 
202
230
  ```ruby
203
- def call
204
- register_stylesheet("stylesheets/user/order_select_component", "data-turbo-track": "reload")
205
- register_stylesheet("stylesheets/admin/dashboard", namespace: :admin)
206
- content_tag(:div, "Content")
231
+ class UserComponent < ApplicationComponent
232
+ include StyleCapsule::ViewComponent
233
+ include StyleCapsule::ViewComponentHelper
234
+ style_capsule namespace: :user # Set default namespace
235
+
236
+ def call
237
+ # Namespace automatically uses :user from style_capsule
238
+ register_stylesheet("stylesheets/user/my_component", "data-turbo-track": "reload")
239
+ content_tag(:div, "Content")
240
+ end
207
241
  end
208
242
  ```
209
243
 
210
- Registered files are rendered via `stylesheet_registry_tags` in your layout, just like inline CSS.
244
+ Registered files are rendered via `stylesheet_registry_tags` in your layout, just like inline CSS. The namespace is automatically used from the component's `style_capsule` configuration when not explicitly specified.
245
+
246
+ **Boot-time file paths:** register static paths once with `StyleCapsule::StylesheetRegistry.register_eager(...)` (initializers, class load). Use `register` / `register_stylesheet` during rendering for request-scoped paths.
247
+
248
+ ### Late head injection (Rails)
249
+
250
+ Layouts usually call `stylesheet_registry_tags` in `<head>` before the body renders. Components that call `register_stylesheet` later in the same response would miss that call without help.
251
+
252
+ By default, `StyleCapsule::HeadInjectionMiddleware` appends pending request-scoped stylesheet tags immediately before `</head>` after the response body is built. Disable when you buffer or stream the body yourself:
253
+
254
+ ```ruby
255
+ # config/application.rb
256
+ config.style_capsule.head_injection_middleware = false
257
+ ```
258
+
259
+ The middleware skips chunked responses (`Transfer-Encoding: chunked`) and does not buffer the body when no pending request-scoped stylesheets remain. For ActionController::Live, SSE, or other streaming HTML, disable it and inject manually with `StyleCapsule::StylesheetRegistry.inject_pending_head_stylesheets` if needed.
211
260
 
212
261
  ## Caching Strategies
213
262
 
@@ -216,22 +265,22 @@ Registered files are rendered via `stylesheet_registry_tags` in your layout, jus
216
265
  ```ruby
217
266
  class MyComponent < ApplicationComponent
218
267
  include StyleCapsule::Component
219
- stylesheet_registry # No cache strategy set (default: :none)
268
+ style_capsule # No cache strategy set (default: :none)
220
269
  end
221
270
  ```
222
271
 
223
272
  ### Time-Based Caching
224
273
 
225
274
  ```ruby
226
- stylesheet_registry cache_strategy: :time, cache_ttl: 1.hour # Using ActiveSupport::Duration
275
+ style_capsule cache_strategy: :time, cache_ttl: 1.hour # Using ActiveSupport::Duration
227
276
  # Or using integer seconds:
228
- stylesheet_registry cache_strategy: :time, cache_ttl: 3600 # Cache for 1 hour
277
+ style_capsule cache_strategy: :time, cache_ttl: 3600 # Cache for 1 hour
229
278
  ```
230
279
 
231
280
  ### Custom Proc Caching
232
281
 
233
282
  ```ruby
234
- stylesheet_registry cache_strategy: ->(css, capsule_id, namespace) {
283
+ style_capsule cache_strategy: ->(css, capsule_id, namespace) {
235
284
  cache_key = "css_#{capsule_id}_#{namespace}"
236
285
  should_cache = css.length > 100
237
286
  expires_at = Time.now + 1800
@@ -248,7 +297,7 @@ Writes CSS to files for HTTP caching. **Requires class method `def self.componen
248
297
  ```ruby
249
298
  class MyComponent < ApplicationComponent
250
299
  include StyleCapsule::Component
251
- stylesheet_registry cache_strategy: :file
300
+ style_capsule cache_strategy: :file
252
301
 
253
302
  # Must use class method for file caching
254
303
  def self.component_styles
@@ -411,79 +460,9 @@ end
411
460
 
412
461
  ## Non-Rails Support
413
462
 
414
- StyleCapsule can be used without Rails! The core functionality is framework-agnostic.
415
-
416
- ### Standalone Usage
463
+ The core library is framework-agnostic. Without Rails, use `StyleCapsule::CssProcessor` directly, `StyleCapsule::Component` with Phlex, or `StyleCapsule::StandaloneHelper` for ERB-style templates. The stylesheet registry falls back to thread-local storage when `ActiveSupport::CurrentAttributes` is not loaded.
417
464
 
418
- ```ruby
419
- require 'style_capsule'
420
-
421
- # Direct CSS processing
422
- css = ".section { color: red; }"
423
- capsule_id = "abc123"
424
- scoped = StyleCapsule::CssProcessor.scope_selectors(css, capsule_id)
425
- # => "[data-capsule=\"abc123\"] .section { color: red; }"
426
- ```
427
-
428
- ### Phlex Without Rails
429
-
430
- ```ruby
431
- require 'phlex'
432
- require 'style_capsule'
433
-
434
- class MyComponent < Phlex::HTML
435
- include StyleCapsule::Component
436
-
437
- def component_styles
438
- <<~CSS
439
- .section { color: red; }
440
- CSS
441
- end
442
-
443
- def view_template
444
- div(class: "section") { "Hello" }
445
- end
446
- end
447
- ```
448
-
449
- ### Sinatra
450
-
451
- ```ruby
452
- require 'sinatra'
453
- require 'style_capsule'
454
-
455
- class MyApp < Sinatra::Base
456
- helpers StyleCapsule::StandaloneHelper
457
-
458
- get '/' do
459
- erb :index
460
- end
461
- end
462
- ```
463
-
464
- ```erb
465
- <!-- views/index.erb -->
466
- <%= style_capsule do %>
467
- <style>
468
- .section { color: red; }
469
- </style>
470
- <div class="section">Content</div>
471
- <% end %>
472
- ```
473
-
474
- ### Stylesheet Registry Without Rails
475
-
476
- The stylesheet registry automatically uses thread-local storage when ActiveSupport is not available:
477
-
478
- ```ruby
479
- require 'style_capsule'
480
-
481
- # Works without Rails
482
- StyleCapsule::StylesheetRegistry.register_inline(".test { color: red; }", namespace: :test)
483
- stylesheets = StyleCapsule::StylesheetRegistry.request_inline_stylesheets
484
- ```
485
-
486
- For more details, see [docs/non_rails_support.md](docs/non_rails_support.md).
465
+ For setup examples, API notes, and the relationship between `style_capsule` and `stylesheet_registry`, see **[docs/non_rails_support.md](docs/non_rails_support.md)**.
487
466
 
488
467
  ## How It Works
489
468
 
@@ -543,3 +522,11 @@ For detailed security information, see [SECURITY.md](SECURITY.md).
543
522
  ## License
544
523
 
545
524
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
525
+
526
+ ## Sponsors
527
+
528
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
529
+
530
+ <a href="https://www.kiskolabs.com">
531
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
532
+ </a>
@@ -0,0 +1,85 @@
1
+ # Non-Rails usage
2
+
3
+ StyleCapsule works without Rails. Core APIs are framework-agnostic; optional pieces integrate with Rails when `railties` and `activesupport` are present.
4
+
5
+ ## Direct CSS processing
6
+
7
+ ```ruby
8
+ require "style_capsule"
9
+
10
+ css = ".section { color: red; }"
11
+ capsule_id = "abc123"
12
+ scoped = StyleCapsule::CssProcessor.scope_selectors(css, capsule_id)
13
+ # => "[data-capsule=\"abc123\"] .section { color: red; }"
14
+ ```
15
+
16
+ ## Phlex (`StyleCapsule::Component`)
17
+
18
+ ```ruby
19
+ require "phlex"
20
+ require "style_capsule"
21
+
22
+ class MyComponent < Phlex::HTML
23
+ include StyleCapsule::Component
24
+
25
+ def component_styles
26
+ <<~CSS
27
+ .section { color: red; }
28
+ CSS
29
+ end
30
+
31
+ def view_template
32
+ div(class: "section") { "Hello" }
33
+ end
34
+ end
35
+ ```
36
+
37
+ ## Sinatra and `StyleCapsule::StandaloneHelper`
38
+
39
+ Use `StyleCapsule::StandaloneHelper` for ERB (or similar) without ActionView. It mirrors the Rails `Helper` API, including `style_capsule(tag: :section)` for a custom wrapper element.
40
+
41
+ ```ruby
42
+ require "sinatra"
43
+ require "style_capsule"
44
+
45
+ class MyApp < Sinatra::Base
46
+ helpers StyleCapsule::StandaloneHelper
47
+
48
+ get "/" do
49
+ erb :index
50
+ end
51
+ end
52
+ ```
53
+
54
+ ```erb
55
+ <!-- views/index.erb -->
56
+ <%= style_capsule do %>
57
+ <style>
58
+ .section { color: red; }
59
+ </style>
60
+ <div class="section">Content</div>
61
+ <% end %>
62
+ ```
63
+
64
+ ## Stylesheet registry without Rails
65
+
66
+ When `ActiveSupport::CurrentAttributes` is not loaded, `StyleCapsule::StylesheetRegistry` uses thread-local storage for request-scoped inline CSS and render-time file paths. Use `register_eager` for process-wide file paths that should appear on every request (boot or class load). Use `register` during rendering when a component calls `register_stylesheet`.
67
+
68
+ ```ruby
69
+ require "style_capsule"
70
+
71
+ # Boot-time / static file (process-wide manifest)
72
+ StyleCapsule::StylesheetRegistry.register_eager("stylesheets/main", namespace: :default)
73
+
74
+ # Render-time registration (same request only)
75
+ StyleCapsule::StylesheetRegistry.register("stylesheets/page", namespace: :default)
76
+
77
+ # Inline CSS (request-scoped)
78
+ StyleCapsule::StylesheetRegistry.register_inline(".test { color: red; }", namespace: :test)
79
+ stylesheets = StyleCapsule::StylesheetRegistry.request_inline_stylesheets
80
+ ```
81
+
82
+ ## Configuration entry points
83
+
84
+ - **`style_capsule`** — Preferred class-level DSL for namespace, cache strategy, CSS scoping (`:selector_patching` or `:nesting`), head rendering, and wrapper `tag:`.
85
+ - **`stylesheet_registry`** — Older alias that enables head rendering and cache options; new code should prefer `style_capsule` for a single configuration surface.
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StyleCapsule
4
+ # Validates logical asset paths for stylesheet registration (blocks injection / absurd paths).
5
+ module AssetPath
6
+ MAX_PATH_LENGTH = 1024
7
+
8
+ # @param path [String] Logical path (e.g. "stylesheets/admin/foo" or "builds/capsules/my_component")
9
+ # @return [String] Stripped path
10
+ # @raise [ArgumentError] If path is invalid
11
+ def self.validate_logical_path!(path)
12
+ unless path.is_a?(String)
13
+ raise ArgumentError, "stylesheet path must be a String (got #{path.class})"
14
+ end
15
+
16
+ s = path.strip
17
+ validate_non_empty_path!(s)
18
+ validate_path_segments!(s, path)
19
+ s
20
+ end
21
+
22
+ def self.validate_non_empty_path!(stripped_path)
23
+ raise ArgumentError, "stylesheet path cannot be empty" if stripped_path.empty?
24
+ raise ArgumentError, "invalid stylesheet path (no leading slash / absolute URL path): #{stripped_path.inspect}" if stripped_path.start_with?("/")
25
+ raise ArgumentError, "invalid stylesheet path (max #{MAX_PATH_LENGTH} characters, got #{stripped_path.length})" if stripped_path.length > MAX_PATH_LENGTH
26
+ end
27
+
28
+ def self.validate_path_segments!(stripped_path, original_path)
29
+ if stripped_path.split("/").any? { |segment| segment == ".." }
30
+ raise ArgumentError, "invalid stylesheet path (parent segments not allowed): #{original_path.inspect}"
31
+ end
32
+
33
+ if /["<>|\0\\]/.match?(stripped_path)
34
+ raise ArgumentError, "invalid stylesheet path (disallowed characters): #{original_path.inspect}"
35
+ end
36
+ end
37
+ private_class_method :validate_non_empty_path!, :validate_path_segments!
38
+ end
39
+ end
@@ -57,11 +57,12 @@ module StyleCapsule
57
57
 
58
58
  # Iterate over all registered classes
59
59
  #
60
+ # Prunes invalid entries on each call (linear in registry size). The set of StyleCapsule
61
+ # components is typically small, so this stays cheap compared to ObjectSpace scans.
62
+ #
60
63
  # @yield [Class] Each registered class
61
64
  # @return [void]
62
65
  def each(&block)
63
- # Filter out classes that no longer exist or have been unloaded
64
- # Use delete_if for Set (equivalent to reject! for Array)
65
66
  @classes.delete_if do |klass|
66
67
  # Check if class still exists and is valid
67
68
  klass.name.nil? || klass.singleton_class?