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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +46 -79
- data/docs/non_rails_support.md +85 -0
- data/lib/style_capsule/asset_path.rb +39 -0
- data/lib/style_capsule/class_registry.rb +3 -2
- data/lib/style_capsule/component.rb +13 -316
- data/lib/style_capsule/component_builder.rb +4 -1
- data/lib/style_capsule/component_class_methods.rb +295 -0
- data/lib/style_capsule/css_file_writer.rb +62 -25
- data/lib/style_capsule/css_processor.rb +4 -6
- data/lib/style_capsule/head_injection_middleware.rb +72 -0
- data/lib/style_capsule/helper.rb +17 -23
- data/lib/style_capsule/helper_scope_cache.rb +72 -0
- data/lib/style_capsule/instrumentation.rb +15 -15
- data/lib/style_capsule/phlex_helper.rb +4 -9
- data/lib/style_capsule/railtie.rb +11 -0
- data/lib/style_capsule/standalone_helper.rb +12 -19
- data/lib/style_capsule/stylesheet_registry.rb +244 -64
- data/lib/style_capsule/version.rb +1 -1
- data/lib/style_capsule/view_component.rb +13 -318
- data/lib/style_capsule/view_component_helper.rb +2 -2
- data/lib/style_capsule.rb +9 -4
- data/lib/tasks/style_capsule.rake +15 -2
- data/sig/style_capsule.rbs +43 -39
- metadata +84 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a44279976aff6cedb8ddc1a8bc4c84382378d73a11ebb7a17850626ecd612745
|
|
4
|
+
data.tar.gz: fee43b83895392f778b7c8b3600dbb5e75e52d660efc76d7939e762ef594e94d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
[](https://badge.fury.io/rb/style_capsule) [](https://github.com/amkisko/style_capsule.rb/actions/workflows/test.yml) [](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
|
-
|
|
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
|
-
|
|
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?
|