style_capsule 1.4.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: 74203df88bc23dc0b1509f0e7adc5978fe327c982026916e990644c92f65a0db
4
- data.tar.gz: 7f7783ef50b66e14859e1f7207262279f20e5f28eb29d3373a85fef9ae2f3a7c
3
+ metadata.gz: a44279976aff6cedb8ddc1a8bc4c84382378d73a11ebb7a17850626ecd612745
4
+ data.tar.gz: fee43b83895392f778b7c8b3600dbb5e75e52d660efc76d7939e762ef594e94d
5
5
  SHA512:
6
- metadata.gz: bed787978d271158ca409ffe8445df1f6203e954000d153beae40368861e0077b566557c3f50456373d04998f85ab60ee86b3cd93ed558d31f3d482247fb0b7e
7
- data.tar.gz: 22b900ebbf5ce4a219d487f7915b7dcd31ed4552d134af44f056ba79eff8551a4ef62e8070f03887fc4962f2073d06bd3b2d65d35e00f09183d97a730ad24ee2
6
+ metadata.gz: a82489272c3ff1e9ac782e2129dc8133a2037101a9719f6c1a38404cec71dae33dab6f64ae93ff6926cd57bcd2456412f87dc61e0c61d04996b226a66466f411
7
+ data.tar.gz: 6c50c563aba3098121e4d9f2693572c6b66cbf6304fff198cafd337ac33da190af06a8469a2a6b59108c1392e2c4b844d13d0dd1cf56025faf5640bbbb1f5bdc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
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
+
3
28
  ## 1.4.0 (2025-11-26)
4
29
 
5
30
  - Added unified `style_capsule` class method for configuring all StyleCapsule settings (namespace, cache strategy, CSS scoping, head rendering) in a single call
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.4.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:
@@ -112,6 +117,15 @@ class MyComponent < ApplicationComponent
112
117
  end
113
118
  ```
114
119
 
120
+ **With custom wrapper tag:**
121
+
122
+ ```ruby
123
+ class MyComponent < ApplicationComponent
124
+ include StyleCapsule::Component
125
+ style_capsule tag: :section # Use <section> instead of <div> for wrapper
126
+ end
127
+ ```
128
+
115
129
  **Global (in base component class):**
116
130
 
117
131
  ```ruby
@@ -229,6 +243,21 @@ end
229
243
 
230
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.
231
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.
260
+
232
261
  ## Caching Strategies
233
262
 
234
263
  ### No Caching (Default)
@@ -431,79 +460,9 @@ end
431
460
 
432
461
  ## Non-Rails Support
433
462
 
434
- StyleCapsule can be used without Rails! The core functionality is framework-agnostic.
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.
435
464
 
436
- ### Standalone Usage
437
-
438
- ```ruby
439
- require 'style_capsule'
440
-
441
- # Direct CSS processing
442
- css = ".section { color: red; }"
443
- capsule_id = "abc123"
444
- scoped = StyleCapsule::CssProcessor.scope_selectors(css, capsule_id)
445
- # => "[data-capsule=\"abc123\"] .section { color: red; }"
446
- ```
447
-
448
- ### Phlex Without Rails
449
-
450
- ```ruby
451
- require 'phlex'
452
- require 'style_capsule'
453
-
454
- class MyComponent < Phlex::HTML
455
- include StyleCapsule::Component
456
-
457
- def component_styles
458
- <<~CSS
459
- .section { color: red; }
460
- CSS
461
- end
462
-
463
- def view_template
464
- div(class: "section") { "Hello" }
465
- end
466
- end
467
- ```
468
-
469
- ### Sinatra
470
-
471
- ```ruby
472
- require 'sinatra'
473
- require 'style_capsule'
474
-
475
- class MyApp < Sinatra::Base
476
- helpers StyleCapsule::StandaloneHelper
477
-
478
- get '/' do
479
- erb :index
480
- end
481
- end
482
- ```
483
-
484
- ```erb
485
- <!-- views/index.erb -->
486
- <%= style_capsule do %>
487
- <style>
488
- .section { color: red; }
489
- </style>
490
- <div class="section">Content</div>
491
- <% end %>
492
- ```
493
-
494
- ### Stylesheet Registry Without Rails
495
-
496
- The stylesheet registry automatically uses thread-local storage when ActiveSupport is not available:
497
-
498
- ```ruby
499
- require 'style_capsule'
500
-
501
- # Works without Rails
502
- StyleCapsule::StylesheetRegistry.register_inline(".test { color: red; }", namespace: :test)
503
- stylesheets = StyleCapsule::StylesheetRegistry.request_inline_stylesheets
504
- ```
505
-
506
- 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)**.
507
466
 
508
467
  ## How It Works
509
468
 
@@ -563,3 +522,11 @@ For detailed security information, see [SECURITY.md](SECURITY.md).
563
522
  ## License
564
523
 
565
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?