style_capsule 2.0.2 → 2.0.3
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/README.md +19 -19
- data/docs/changelogs/20260904180000_audit-fixes.md +5 -1
- data/docs/changelogs/20260925094000_head-injection-bufferable-chunked.md +1 -1
- data/docs/changelogs/20260928193000_sync-appraisal-locks-after-version-bump.md +19 -0
- data/docs/issues/20260904174500_engineering-and-dependency-audit.md +5 -1
- data/docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md +1 -1
- data/docs/issues/20261103150931_engineering-prose-release-audit.md +40 -0
- data/docs/non_rails_support.md +2 -2
- data/docs/reviews/20261028_engineering-audit-release-preparation.md +25 -0
- data/docs/reviews/20261103150931_engineering-prose-release-audit.md +42 -0
- data/lib/style_capsule/component.rb +1 -1
- data/lib/style_capsule/component_builder.rb +1 -1
- data/lib/style_capsule/component_styles_support.rb +10 -0
- data/lib/style_capsule/css_processor.rb +205 -82
- data/lib/style_capsule/head_injection_middleware.rb +4 -1
- data/lib/style_capsule/stylesheet_registry.rb +8 -2
- data/lib/style_capsule/version.rb +1 -1
- data/lib/style_capsule/view_component.rb +1 -1
- data/sig/style_capsule.rbs +4 -1
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a6c3fda27b85ee3899cbe434dd5230de0d1c9c4ff6c961deefa9b058fd8f6b3
|
|
4
|
+
data.tar.gz: e70f42c7ee1fc234201107862e1c7dfe9f609a9a55333983101aed4d1ec74ff7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 163cd5d8f4f0a324121a866adc94c48f31438cd560ba2b30b2d8f3721b645fe4a339e89308cb38552a96294fd8b01a834936615617ff6889203003f593c4e716
|
|
7
|
+
data.tar.gz: 39ca2e8c56745ddcbeae553b401e3e1b0a4b92409f0d066f0a50a9309bc3efac5369f38e929e6d04f0df991b8abbbe30723602c7f5458ac152a4bdf67ad4dc3a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 2.0.3 (2026-11-03)
|
|
4
|
+
|
|
5
|
+
- Leave `@keyframes` / `@font-face` / `@property` steps unscoped under selector patching
|
|
6
|
+
- Hoist non-nestable at-rules outside the CSS nesting wrapper
|
|
7
|
+
- Clear pending request stylesheets when head injection cannot find `</head>`
|
|
8
|
+
- Replace ActiveSupport `blank?` on core CSS and content-type checks
|
|
9
|
+
- Warn once when `:file` cache strategy falls back to `:none` without class-method `component_styles`
|
|
10
|
+
- Fail closed when release confirmation input reaches EOF; accept case-insensitive `y`
|
|
11
|
+
- Require a dated CHANGELOG heading for the release version before `gem build`
|
|
12
|
+
- Run the test workflow on pull requests targeting `main`
|
|
13
|
+
|
|
3
14
|
## 2.0.2 (2026-09-25)
|
|
4
15
|
|
|
5
16
|
- Fix `HeadInjectionMiddleware` dropping pending stylesheets on bufferable chunked HTML
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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://app.codecov.io/github/amkisko/style_capsule.rb)
|
|
4
4
|
|
|
5
|
-
CSS scoping
|
|
5
|
+
CSS scoping for Ruby components. Attribute selectors encapsulate styles for Phlex, ViewComponent, and ERB so rules stay inside the component wrapper. Works with Rails and standalone in other Ruby frameworks (Sinatra, Hanami, and similar) or plain Ruby. Cache strategies: none, time-based, custom proc, and file-based.
|
|
6
6
|
|
|
7
7
|
Design changes go through `rfcs/` (RFC 0001).
|
|
8
8
|
|
|
@@ -18,15 +18,15 @@ Then run `bundle install`.
|
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
21
|
+
- Attribute-based CSS scoping (class names stay as written)
|
|
22
|
+
- Phlex, ViewComponent, and ERB; Rails integration via the railtie
|
|
23
|
+
- Per-component-type scope IDs (shared across instances)
|
|
24
|
+
- Optional CSS nesting (`:nesting`) for browsers that support it
|
|
25
|
+
- Stylesheet registry for head rendering, namespaces, and asset bundlers such as Propshaft
|
|
26
|
+
- Cache strategies: none, time-based, custom proc, and file-based
|
|
27
|
+
- `ActiveSupport::Notifications` events for CSS processing and file writes
|
|
28
|
+
- Fallback output directory when the primary path is not writable
|
|
29
|
+
- Path traversal checks, input validation, and CSS size limits
|
|
30
30
|
|
|
31
31
|
## Usage
|
|
32
32
|
|
|
@@ -103,8 +103,8 @@ StyleCapsule supports two CSS scoping strategies:
|
|
|
103
103
|
- Better browser support (all modern browsers)
|
|
104
104
|
- Output: `[data-capsule="abc123"] .section { color: red; }`
|
|
105
105
|
|
|
106
|
-
2. **CSS Nesting (optional)**: Wraps
|
|
107
|
-
-
|
|
106
|
+
2. **CSS Nesting (optional)**: Wraps nestable rules in `[data-capsule="..."] { ... }`
|
|
107
|
+
- Skips per-selector rewrite; hoists `@keyframes` / `@font-face` and similar to top level
|
|
108
108
|
- Requires CSS nesting support (Chrome 112+, Firefox 117+, Safari 16.5+)
|
|
109
109
|
- Output: `[data-capsule="abc123"] { .section { color: red; } }`
|
|
110
110
|
|
|
@@ -145,7 +145,7 @@ MyComponent.clear_css_cache
|
|
|
145
145
|
|
|
146
146
|
## Stylesheet Registry
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Register styles for head rendering when the layout should emit links or shared style tags instead of body `<style>` tags. Configure with `style_capsule`:
|
|
149
149
|
|
|
150
150
|
```ruby
|
|
151
151
|
class MyComponent < ApplicationComponent
|
|
@@ -323,7 +323,7 @@ StyleCapsule::CssFileWriter.configure(
|
|
|
323
323
|
)
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
-
**Fallback Directory:**
|
|
326
|
+
**Fallback Directory:** When the primary output directory is not writable (for example a read-only app tree in a container), writes go under a per-user directory in `Dir.tmpdir`. If that write also fails, rendering uses inline CSS.
|
|
327
327
|
|
|
328
328
|
**Precompilation:**
|
|
329
329
|
|
|
@@ -338,7 +338,7 @@ Files are automatically built during `bin/rails assets:precompile`.
|
|
|
338
338
|
|
|
339
339
|
## Instrumentation
|
|
340
340
|
|
|
341
|
-
|
|
341
|
+
Subscribe to `ActiveSupport::Notifications` for CSS processing and file write events. Metric payloads are built only when a subscriber is present.
|
|
342
342
|
|
|
343
343
|
### Available Events
|
|
344
344
|
|
|
@@ -469,9 +469,9 @@ For setup examples, API notes, and the relationship between `style_capsule` and
|
|
|
469
469
|
## How It Works
|
|
470
470
|
|
|
471
471
|
1. **Scope ID Generation**: Each component class gets a unique scope ID based on its class name (shared across all instances)
|
|
472
|
-
2. **CSS Rewriting**: CSS selectors
|
|
473
|
-
3. **HTML Wrapping**: Component content is
|
|
474
|
-
4. **
|
|
472
|
+
2. **CSS Rewriting**: CSS selectors gain `[data-capsule="..."]` attribute selectors (or a nesting wrapper)
|
|
473
|
+
3. **HTML Wrapping**: Component content is wrapped in a scoped element
|
|
474
|
+
4. **Class names**: Class names in CSS and markup stay as written
|
|
475
475
|
|
|
476
476
|
## Development
|
|
477
477
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# Audit fixes for style_capsule 2.0.1
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
4
6
|
|
|
5
7
|
## Decisions
|
|
6
8
|
|
|
9
|
+
Recorded 2026-09-04. Implements the ranked findings from docs/issues/20260904174500_engineering-and-dependency-audit.md. Shipped in product CHANGELOG 2.0.2. json lockfile bump is recorded in docs/dependencies/20260904174500_json-cve-2026-71847.md.
|
|
10
|
+
|
|
7
11
|
Treat stored CSS as untrusted at the style-tag boundary. CssProcessor.reject_style_element_breakout! matches a case-insensitive style closer and raises ArgumentError. Call it after the size check in scope_selectors and scope_with_nesting, and again in render_inline_stylesheet. Empty CSS still returns early without that check.
|
|
8
12
|
|
|
9
13
|
Allowlist non-Rails fallback link attributes. Emit only media, title, crossorigin, integrity, nonce, type, id, class, and data-* names. Escape values with CGI.escapeHTML. Never emit option href, rel, or event handlers. Keep href as /assets/#{validated_path}.css.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## Participants
|
|
2
|
+
|
|
3
|
+
Andrei Makarov (@amkisko)
|
|
4
|
+
|
|
5
|
+
## Decisions
|
|
6
|
+
|
|
7
|
+
Sync appraisal lockfiles to style_capsule 2.0.2 after the release bump left gemfiles/*.gemfile.lock on 2.0.1. Change usr/bin/release.rb to run appraisal install so path-gem version changes update those locks before the next push.
|
|
8
|
+
|
|
9
|
+
## Effects
|
|
10
|
+
|
|
11
|
+
test #37 on main (c41eb95) failed: all three matrix jobs exited bundle with code 16 under frozen install because the gemspec version no longer matched the appraisal locks. quality used Gemfile.lock (already 2.0.2) and passed. After updating the three locks, BUNDLE_FROZEN=true bundle check succeeds for each appraisal gemfile. BUNDLE_GEMFILE=gemfiles/rails8ruby34.gemfile bundle exec polyrun parallel-rspec --workers 5 --merge-failures exited 0.
|
|
12
|
+
|
|
13
|
+
## Next
|
|
14
|
+
|
|
15
|
+
Push patch/sync-appraisal-locks-2-0-2 and confirm the test workflow on main after merge.
|
|
16
|
+
|
|
17
|
+
## Source
|
|
18
|
+
|
|
19
|
+
https://github.com/amkisko/style_capsule.rb/actions/runs/36104324632
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# Engineering and dependency audit of style_capsule 2.0.1
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
4
6
|
|
|
5
7
|
## Decisions
|
|
6
8
|
|
|
9
|
+
Recorded 2026-09-04. Library gem. No product UI, database, queue, or worker. Pipeline is CSS and HTML in, processor and helpers, class and thread caches, optional file writer, stylesheet registry, optional Rack head injection, HTML out.
|
|
10
|
+
|
|
7
11
|
This pass is an audit only. No code change, no lockfile bump, no workflow edit. Rankings follow engineering-audit order: danger, then certainty, then impact, then fix cost. Dependency-audit ran because the request named both. The published gemspec has no runtime gems. json 2.21.1 is a development lockfile pin from activesupport and rubocop, not a consumer runtime of this gem.
|
|
8
12
|
|
|
9
13
|
## Effects
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Engineering and prose audit before 2.0.3 publish
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
Re-audit main for the unpublished 2.0.3 cut after the 2026-10-28 release-tooling pass. Include CssProcessor keyframes and nesting hoist, pending head clear when `</head>` is missing, ActiveSupport-free blank checks, `:file` cache fallback warning, release confirmation and changelog gates, pull_request CI, and ASCII docs punctuation. Leave tag and RubyGems publish to `make release` after commit.
|
|
10
|
+
|
|
11
|
+
## Effects
|
|
12
|
+
|
|
13
|
+
Pipeline: component/helper render -> CssProcessor -> StylesheetRegistry / CssFileWriter -> HeadInjectionMiddleware -> HTML. Boundary mode covered Rack body buffering and filesystem writes. Resource and identification modes: no new person identifiers in gem logs; cold-require numbers stay session observations. Queue, worker, learned-system, and physical plant modes skipped (absent).
|
|
14
|
+
|
|
15
|
+
Ranked fixes applied:
|
|
16
|
+
|
|
17
|
+
1. Skip selector patching inside `@keyframes` / `@font-face` / `@property` (high confidence, observed; prior keyframes example was futile).
|
|
18
|
+
2. Hoist non-nestable at-rules outside the nesting wrapper (high confidence, observed).
|
|
19
|
+
3. Clear pending request stylesheets when inject cannot find `</head>` or tags are empty (high confidence, observed).
|
|
20
|
+
4. Replace `blank?` with nil/empty checks in CssProcessor and HeadInjectionMiddleware (high confidence, observed).
|
|
21
|
+
5. Warn once when `:file` falls back to `:none` without class-method `component_styles` (high confidence, observed).
|
|
22
|
+
6. Changelog heading and confirmation specs; CHANGELOG dated 2026-11-03; prose pass on README, gemspec, docs/non_rails_support.md, and builder skip warn punctuation.
|
|
23
|
+
|
|
24
|
+
Deferred:
|
|
25
|
+
|
|
26
|
+
- Class-level CSS cache mutex under threaded hosts.
|
|
27
|
+
- Deduping Phlex / ViewComponent / helper render paths and stylesheet_registry split.
|
|
28
|
+
- `component_styles` double evaluation per render.
|
|
29
|
+
- CssFileWriter positive-path cache revalidation after external delete.
|
|
30
|
+
- Optional max-bytes skip for middleware buffering.
|
|
31
|
+
|
|
32
|
+
## Next
|
|
33
|
+
|
|
34
|
+
Commit, then `make release`.
|
|
35
|
+
|
|
36
|
+
## Source
|
|
37
|
+
|
|
38
|
+
- docs/reviews/20261103150931_engineering-prose-release-audit.md
|
|
39
|
+
- CHANGELOG.md section 2.0.3 (2026-11-03)
|
|
40
|
+
- Prior pass: docs/reviews/20261028_engineering-audit-release-preparation.md
|
data/docs/non_rails_support.md
CHANGED
|
@@ -81,5 +81,5 @@ stylesheets = StyleCapsule::StylesheetRegistry.request_inline_stylesheets
|
|
|
81
81
|
|
|
82
82
|
## Configuration entry points
|
|
83
83
|
|
|
84
|
-
-
|
|
85
|
-
-
|
|
84
|
+
- `style_capsule` - Class-level DSL for namespace, cache strategy, CSS scoping (`:selector_patching` or `:nesting`), head rendering, and wrapper `tag:`.
|
|
85
|
+
- `stylesheet_registry` - Older alias for head rendering and cache options; use `style_capsule` in new code.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Engineering audit and release preparation
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
- Treat end-of-input at the release confirmation prompt as rejection.
|
|
10
|
+
- Keep the ViewComponent loading-error test isolated from unrelated class ancestry checks.
|
|
11
|
+
- Run tests for pull requests targeting `main`.
|
|
12
|
+
|
|
13
|
+
## Effects
|
|
14
|
+
|
|
15
|
+
- `make test`, RBS validation, RuboCop, and the sharded suite pass.
|
|
16
|
+
- The 2.0.3 gem builds and passes `Gem::Package#verify`.
|
|
17
|
+
- A local cold require used 0.398 CPU seconds and 33,308,672 bytes peak RSS. These are single-machine observations, not release budgets.
|
|
18
|
+
|
|
19
|
+
## Next
|
|
20
|
+
|
|
21
|
+
- Review the worktree, commit it, and let the release workflow create the tag and publish the gem.
|
|
22
|
+
|
|
23
|
+
## Source
|
|
24
|
+
|
|
25
|
+
Iterative engineering audit on 2026-10-28.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Engineering and prose audit before 2.0.3 publish
|
|
2
|
+
|
|
3
|
+
## Participants
|
|
4
|
+
|
|
5
|
+
Andrei Makarov (@amkisko)
|
|
6
|
+
|
|
7
|
+
## Decisions
|
|
8
|
+
|
|
9
|
+
Re-audit main for the unpublished 2.0.3 cut. Include member CssProcessor keyframes and nesting, pending head clear, blank checks, file-cache warn, release gates, pull_request CI, and ASCII docs punctuation. Leave tag and RubyGems publish to make release after commit.
|
|
10
|
+
|
|
11
|
+
## Effects
|
|
12
|
+
|
|
13
|
+
Pipeline: render -> CssProcessor -> registry / file writer -> head inject -> HTML. Boundary mode covered Rack buffering and disk writes. Resource and identification modes: no new person identifiers in gem logs. Queue, worker, learned-system, and physical plant modes skipped (absent).
|
|
14
|
+
|
|
15
|
+
Ranked fixes applied:
|
|
16
|
+
|
|
17
|
+
1. Unscoped `@keyframes` / `@font-face` / `@property` steps under selector patching (high confidence, observed).
|
|
18
|
+
2. Hoist non-nestable at-rules outside nesting wrapper (high confidence, observed).
|
|
19
|
+
3. Clear pending request stylesheets when `</head>` is missing (high confidence, observed).
|
|
20
|
+
4. Nil/empty checks instead of ActiveSupport `blank?` on core paths (high confidence, observed).
|
|
21
|
+
5. Warn once on illegal `:file` cache fallback (high confidence, observed).
|
|
22
|
+
6. Changelog heading and confirmation specs; CHANGELOG dated 2026-11-03.
|
|
23
|
+
7. ASCII and marketing-tone pass on README, CHANGELOG, gemspec, and docs/non_rails_support.md.
|
|
24
|
+
|
|
25
|
+
Deferred (documented in the issue): class CSS cache mutex, helper/component dedupe, double `component_styles` eval, file-path cache revalidation, middleware max-bytes.
|
|
26
|
+
|
|
27
|
+
Checks observed in this session:
|
|
28
|
+
|
|
29
|
+
- bundle exec rubocop on touched Ruby files: 11 files, no offenses.
|
|
30
|
+
- bundle exec rbs validate: ok.
|
|
31
|
+
- bundle exec rspec on css_processor, stylesheet_registry, head_injection_middleware, component, view_component, release_version_check: 249 examples, 0 failures.
|
|
32
|
+
- bundle exec polyrun parallel-rspec --workers 5 --merge-failures: exit 0; coverage/merged-summary.txt 97.00% lines (1101 / 1135) across 19 files.
|
|
33
|
+
- gem build style_capsule.gemspec and Gem::Package#verify for 2.0.3: ok.
|
|
34
|
+
|
|
35
|
+
## Next
|
|
36
|
+
|
|
37
|
+
Commit, then make release.
|
|
38
|
+
|
|
39
|
+
## Source
|
|
40
|
+
|
|
41
|
+
- docs/issues/20261103150931_engineering-prose-release-audit.md
|
|
42
|
+
- CHANGELOG.md section 2.0.3 (2026-11-03)
|
|
@@ -168,7 +168,7 @@ module StyleCapsule
|
|
|
168
168
|
|
|
169
169
|
# File caching is only allowed for class method component_styles
|
|
170
170
|
if cache_strategy == :file && !file_caching_allowed?
|
|
171
|
-
|
|
171
|
+
warn_file_cache_fallback(self.class)
|
|
172
172
|
cache_strategy = :none
|
|
173
173
|
cache_ttl = nil
|
|
174
174
|
cache_proc = nil
|
|
@@ -113,7 +113,7 @@ module StyleCapsule
|
|
|
113
113
|
file_path
|
|
114
114
|
rescue ArgumentError, NoMethodError => e
|
|
115
115
|
# Component requires arguments or has dependencies - skip it (common for ViewComponent with required kwargs)
|
|
116
|
-
warn "[style_capsule] Skipped #{component_class.name} in style_capsule:build
|
|
116
|
+
warn "[style_capsule] Skipped #{component_class.name} in style_capsule:build - #{e.class}: #{e.message}"
|
|
117
117
|
output_proc&.call("Skipped #{component_class.name}: #{e.message}")
|
|
118
118
|
nil
|
|
119
119
|
end
|
|
@@ -48,6 +48,16 @@ module StyleCapsule
|
|
|
48
48
|
|
|
49
49
|
private
|
|
50
50
|
|
|
51
|
+
def warn_file_cache_fallback(component_class)
|
|
52
|
+
return if component_class.instance_variable_get(:@_style_capsule_file_cache_warned)
|
|
53
|
+
|
|
54
|
+
component_class.instance_variable_set(:@_style_capsule_file_cache_warned, true)
|
|
55
|
+
label = component_class.name
|
|
56
|
+
label = component_class.inspect if label.nil? || label.empty?
|
|
57
|
+
warn "[StyleCapsule] :file cache strategy requires class-method component_styles; " \
|
|
58
|
+
"falling back to :none for #{label}"
|
|
59
|
+
end
|
|
60
|
+
|
|
51
61
|
# Check if component defines instance method styles
|
|
52
62
|
#
|
|
53
63
|
# @return [Boolean]
|
|
@@ -7,15 +7,25 @@ module StyleCapsule
|
|
|
7
7
|
#
|
|
8
8
|
# Supports two scoping strategies:
|
|
9
9
|
# 1. Selector patching (default): Adds [data-capsule="..."] prefix to each selector
|
|
10
|
-
# -
|
|
10
|
+
# - Wider browser support (all modern browsers)
|
|
11
11
|
# - Requires CSS parsing and transformation
|
|
12
|
-
# 2. CSS nesting (optional): Wraps
|
|
13
|
-
# -
|
|
12
|
+
# 2. CSS nesting (optional): Wraps style rules in [data-capsule="..."] { ... }
|
|
13
|
+
# - Avoids per-selector rewrite
|
|
14
14
|
# - Requires CSS nesting support (Chrome 112+, Firefox 117+, Safari 16.5+)
|
|
15
|
+
# - Hoists non-nestable at-rules (@keyframes, @font-face, @import, ...) to top level
|
|
16
|
+
# rubocop:disable Metrics/ModuleLength -- processor owns patching, nesting hoist, and validation
|
|
15
17
|
module CssProcessor
|
|
16
18
|
# Maximum CSS content size (1MB) to prevent DoS attacks
|
|
17
19
|
MAX_CSS_SIZE = 1_000_000
|
|
18
20
|
|
|
21
|
+
# At-rules whose inner selectors must not receive attribute scoping
|
|
22
|
+
NON_SCOPING_AT_RULES = %w[keyframes font-face property].freeze
|
|
23
|
+
|
|
24
|
+
# At-rules that must stay at the top level under CSS nesting
|
|
25
|
+
HOISTABLE_AT_RULES = %w[
|
|
26
|
+
keyframes font-face import charset namespace property
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
19
29
|
# Rewrite CSS selectors to include attribute-based scoping
|
|
20
30
|
#
|
|
21
31
|
# Transforms:
|
|
@@ -26,25 +36,17 @@ module StyleCapsule
|
|
|
26
36
|
# [data-capsule="a1b2c3d4"] .section { color: red; }
|
|
27
37
|
# [data-capsule="a1b2c3d4"] .heading:hover { opacity: 0.8; }
|
|
28
38
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# Simple approach:
|
|
33
|
-
# - Strips CSS comments first to avoid interference
|
|
34
|
-
# - Finds selectors before opening braces and prefixes them
|
|
35
|
-
# - Handles @media queries (preserves them, scopes inner selectors)
|
|
36
|
-
# - Handles :host and :host-context (component-scoped selectors)
|
|
39
|
+
# Uses attribute selectors instead of renaming classes, so styles apply only
|
|
40
|
+
# within scoped components. Leaves @keyframes / @font-face / @property steps alone.
|
|
37
41
|
#
|
|
38
42
|
# @param css_string [String] Original CSS content
|
|
39
43
|
# @param capsule_id [String] The capsule ID to use in attribute selector
|
|
40
44
|
# @param component_class [Class, String, nil] Optional component class for instrumentation
|
|
41
45
|
# @return [String] CSS with scoped selectors
|
|
42
46
|
# @raise [ArgumentError] If CSS content exceeds maximum size or capsule_id is invalid
|
|
43
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- selector patching walks CSS rules
|
|
44
47
|
def self.scope_selectors(css_string, capsule_id, component_class: nil)
|
|
45
48
|
return css_string if css_string.nil? || css_string.strip.empty?
|
|
46
49
|
|
|
47
|
-
# Validate CSS size to prevent DoS attacks
|
|
48
50
|
if css_string.bytesize > MAX_CSS_SIZE
|
|
49
51
|
raise ArgumentError, "CSS content exceeds maximum size of #{MAX_CSS_SIZE} bytes (got #{css_string.bytesize} bytes)"
|
|
50
52
|
end
|
|
@@ -52,72 +54,22 @@ module StyleCapsule
|
|
|
52
54
|
reject_style_element_breakout!(css_string)
|
|
53
55
|
validate_capsule_id!(capsule_id)
|
|
54
56
|
|
|
55
|
-
# Instrument CSS processing with timing and size metrics
|
|
56
57
|
Instrumentation.instrument_css_processing(
|
|
57
58
|
strategy: :selector_patching,
|
|
58
59
|
component_class: component_class || "Unknown",
|
|
59
60
|
capsule_id: capsule_id,
|
|
60
61
|
css_content: css_string
|
|
61
62
|
) do
|
|
62
|
-
css = css_string.dup
|
|
63
63
|
capsule_attr = %([data-capsule="#{capsule_id}"])
|
|
64
|
-
|
|
65
|
-
# Strip CSS comments so they do not interfere with selector matching.
|
|
66
|
-
# Comments are removed from the output (typical for production CSS).
|
|
67
|
-
css_without_comments = strip_comments(css)
|
|
68
|
-
|
|
69
|
-
# Process CSS rule by rule
|
|
70
|
-
# Match: selector(s) { ... }
|
|
71
|
-
# Pattern: (start or closing brace) + (whitespace) + (selector text) + (opening brace)
|
|
72
|
-
# Note: Uses non-greedy quantifier ([^{}@]+?) to minimize backtracking
|
|
73
|
-
# MAX_CSS_SIZE limit (1MB) mitigates ReDoS risk from malicious input
|
|
74
|
-
css_without_comments.gsub!(/(^|\})(\s*)([^{}@]+?)(\{)/m) do |_|
|
|
75
|
-
prefix = Regexp.last_match(1) # Previous closing brace or start
|
|
76
|
-
whitespace = Regexp.last_match(2) # Whitespace between rules
|
|
77
|
-
selectors_raw = Regexp.last_match(3) # The selector group
|
|
78
|
-
selectors = selectors_raw.strip # Stripped for processing
|
|
79
|
-
opening_brace = Regexp.last_match(4) # The opening brace
|
|
80
|
-
|
|
81
|
-
# Skip at-rules (@media, @keyframes, etc.) - they should not be scoped at top level
|
|
82
|
-
next "#{prefix}#{whitespace}#{selectors_raw}#{opening_brace}" if selectors.start_with?("@")
|
|
83
|
-
|
|
84
|
-
# Skip if already scoped (avoid double-scoping)
|
|
85
|
-
next "#{prefix}#{whitespace}#{selectors_raw}#{opening_brace}" if selectors_raw.include?("[data-capsule=")
|
|
86
|
-
|
|
87
|
-
# Skip empty selectors
|
|
88
|
-
next "#{prefix}#{whitespace}#{selectors_raw}#{opening_brace}" if selectors.empty?
|
|
89
|
-
|
|
90
|
-
# Split selectors by comma and scope each one
|
|
91
|
-
scoped_selectors = selectors.split(",").map do |selector|
|
|
92
|
-
selector = selector.strip
|
|
93
|
-
next selector if selector.empty?
|
|
94
|
-
|
|
95
|
-
# Handle special component-scoped selectors (:host, :host-context)
|
|
96
|
-
if selector.start_with?(":host")
|
|
97
|
-
selector = selector
|
|
98
|
-
.gsub(/^:host-context\(([^)]+)\)/, "#{capsule_attr} \\1")
|
|
99
|
-
.gsub(/^:host\(([^)]+)\)/, "#{capsule_attr}\\1")
|
|
100
|
-
.gsub(/^:host\b/, capsule_attr)
|
|
101
|
-
selector
|
|
102
|
-
else
|
|
103
|
-
# Add capsule attribute with space before selector for descendant matching
|
|
104
|
-
# This ensures styles apply to elements inside the scoped wrapper
|
|
105
|
-
"#{capsule_attr} #{selector}"
|
|
106
|
-
end
|
|
107
|
-
end.compact.join(", ")
|
|
108
|
-
|
|
109
|
-
"#{prefix}#{whitespace}#{scoped_selectors}#{opening_brace}"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
css_without_comments
|
|
64
|
+
patch_selectors(strip_comments(css_string.dup), capsule_attr)
|
|
113
65
|
end
|
|
114
66
|
end
|
|
115
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
116
67
|
|
|
117
|
-
# Scope CSS using CSS nesting (wraps
|
|
68
|
+
# Scope CSS using CSS nesting (wraps nestable rules in [data-capsule] { ... })
|
|
118
69
|
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
70
|
+
# Avoids per-selector rewrite. Requires CSS nesting support in browsers
|
|
71
|
+
# (Chrome 112+, Firefox 117+, Safari 16.5+). Non-nestable at-rules are written
|
|
72
|
+
# before the wrapper so @keyframes and @font-face stay valid.
|
|
121
73
|
#
|
|
122
74
|
# Transforms:
|
|
123
75
|
# .section { color: red; }
|
|
@@ -132,12 +84,12 @@ module StyleCapsule
|
|
|
132
84
|
# @param css_string [String] Original CSS content
|
|
133
85
|
# @param capsule_id [String] The capsule ID to use in attribute selector
|
|
134
86
|
# @param component_class [Class, String, nil] Optional component class for instrumentation
|
|
135
|
-
# @return [String] CSS
|
|
87
|
+
# @return [String] CSS with nesting wrapper and hoisted at-rules
|
|
136
88
|
# @raise [ArgumentError] If CSS content exceeds maximum size or capsule_id is invalid
|
|
89
|
+
# rubocop:disable Metrics/AbcSize -- size check, breakout, instrument, then hoist and wrap
|
|
137
90
|
def self.scope_with_nesting(css_string, capsule_id, component_class: nil)
|
|
138
91
|
return css_string if css_string.nil? || css_string.strip.empty?
|
|
139
92
|
|
|
140
|
-
# Validate CSS size to prevent DoS attacks
|
|
141
93
|
if css_string.bytesize > MAX_CSS_SIZE
|
|
142
94
|
raise ArgumentError, "CSS content exceeds maximum size of #{MAX_CSS_SIZE} bytes (got #{css_string.bytesize} bytes)"
|
|
143
95
|
end
|
|
@@ -145,27 +97,27 @@ module StyleCapsule
|
|
|
145
97
|
reject_style_element_breakout!(css_string)
|
|
146
98
|
validate_capsule_id!(capsule_id)
|
|
147
99
|
|
|
148
|
-
# Instrument CSS processing with timing and size metrics
|
|
149
100
|
Instrumentation.instrument_css_processing(
|
|
150
101
|
strategy: :nesting,
|
|
151
102
|
component_class: component_class || "Unknown",
|
|
152
103
|
capsule_id: capsule_id,
|
|
153
104
|
css_content: css_string
|
|
154
105
|
) do
|
|
155
|
-
# Simply wrap the entire CSS in the capsule attribute selector
|
|
156
|
-
# No parsing or transformation needed - much more performant
|
|
157
106
|
capsule_attr = %([data-capsule="#{capsule_id}"])
|
|
158
|
-
|
|
107
|
+
hoisted, nestable = hoist_top_level_at_rules(css_string)
|
|
108
|
+
parts = []
|
|
109
|
+
parts << hoisted unless hoisted.empty?
|
|
110
|
+
parts << "#{capsule_attr} {\n#{nestable}\n}" unless nestable.strip.empty?
|
|
111
|
+
parts.join("\n")
|
|
159
112
|
end
|
|
160
113
|
end
|
|
114
|
+
# rubocop:enable Metrics/AbcSize
|
|
161
115
|
|
|
162
116
|
# Strip CSS comments (/* ... */) from the string
|
|
163
117
|
#
|
|
164
118
|
# @param css [String] CSS content with comments
|
|
165
119
|
# @return [String] CSS content without comments
|
|
166
120
|
def self.strip_comments(css)
|
|
167
|
-
# Remove /* ... */ comments (including multi-line)
|
|
168
|
-
# Use non-greedy match to handle multiple comments
|
|
169
121
|
css.gsub(/\/\*.*?\*\//m, "")
|
|
170
122
|
end
|
|
171
123
|
|
|
@@ -177,7 +129,10 @@ module StyleCapsule
|
|
|
177
129
|
# @param css_string [String, nil] CSS that will be placed inside a style tag
|
|
178
130
|
# @raise [ArgumentError] If the string contains a style element closer
|
|
179
131
|
def self.reject_style_element_breakout!(css_string)
|
|
180
|
-
|
|
132
|
+
# Core path must not call ActiveSupport String#blank?
|
|
133
|
+
# rubocop:disable Rails/Blank -- standalone hosts may lack ActiveSupport
|
|
134
|
+
return if css_string.nil? || css_string.empty?
|
|
135
|
+
# rubocop:enable Rails/Blank
|
|
181
136
|
return unless css_string.match?(%r{</style}i)
|
|
182
137
|
|
|
183
138
|
raise ArgumentError, "CSS content must not contain a style element closer"
|
|
@@ -192,23 +147,191 @@ module StyleCapsule
|
|
|
192
147
|
raise ArgumentError, "capsule_id must be a String (got #{capsule_id.class})"
|
|
193
148
|
end
|
|
194
149
|
|
|
195
|
-
# Must not be empty (check first for clearer error message)
|
|
196
150
|
if capsule_id.empty?
|
|
197
151
|
raise ArgumentError, "capsule_id cannot be empty"
|
|
198
152
|
end
|
|
199
153
|
|
|
200
|
-
# Capsule ID should only contain alphanumeric characters, hyphens, and underscores
|
|
201
|
-
# This prevents injection into HTML attributes
|
|
202
154
|
unless capsule_id.match?(/\A[a-zA-Z0-9_-]+\z/)
|
|
203
155
|
raise ArgumentError, "Invalid capsule_id: must contain only alphanumeric characters, hyphens, and underscores (got: #{capsule_id.inspect})"
|
|
204
156
|
end
|
|
205
157
|
|
|
206
|
-
# Reasonable length limit
|
|
207
158
|
if capsule_id.length > 100
|
|
208
159
|
raise ArgumentError, "Invalid capsule_id: too long (max 100 characters, got #{capsule_id.length})"
|
|
209
160
|
end
|
|
210
161
|
end
|
|
211
162
|
|
|
212
|
-
|
|
163
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- walks braces and at-rules in one pass
|
|
164
|
+
def self.patch_selectors(css, capsule_attr)
|
|
165
|
+
output = +""
|
|
166
|
+
index = 0
|
|
167
|
+
length = css.length
|
|
168
|
+
brace_depth = 0
|
|
169
|
+
suppress_below_depth = nil
|
|
170
|
+
|
|
171
|
+
while index < length
|
|
172
|
+
while index < length && css[index].match?(/\s/)
|
|
173
|
+
output << css[index]
|
|
174
|
+
index += 1
|
|
175
|
+
end
|
|
176
|
+
break if index >= length
|
|
177
|
+
|
|
178
|
+
if css[index] == "}"
|
|
179
|
+
output << "}"
|
|
180
|
+
index += 1
|
|
181
|
+
brace_depth -= 1
|
|
182
|
+
if !suppress_below_depth.nil? && brace_depth <= suppress_below_depth
|
|
183
|
+
suppress_below_depth = nil
|
|
184
|
+
end
|
|
185
|
+
next
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
selector_start = index
|
|
189
|
+
while index < length && css[index] != "{" && css[index] != "}"
|
|
190
|
+
index += 1
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
selectors_raw = css[selector_start...index]
|
|
194
|
+
if index >= length || css[index] == "}"
|
|
195
|
+
output << selectors_raw
|
|
196
|
+
next
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
selectors = selectors_raw.strip
|
|
200
|
+
at_name = selectors[/\A@([a-zA-Z-]+)/, 1]&.downcase
|
|
201
|
+
entering_non_scoping = at_name && NON_SCOPING_AT_RULES.include?(at_name) && suppress_below_depth.nil?
|
|
202
|
+
|
|
203
|
+
skip_scoping = !suppress_below_depth.nil? ||
|
|
204
|
+
selectors.start_with?("@") ||
|
|
205
|
+
selectors_raw.include?("[data-capsule=") ||
|
|
206
|
+
selectors.empty?
|
|
207
|
+
|
|
208
|
+
if skip_scoping
|
|
209
|
+
output << selectors_raw
|
|
210
|
+
if entering_non_scoping
|
|
211
|
+
suppress_below_depth = brace_depth
|
|
212
|
+
end
|
|
213
|
+
else
|
|
214
|
+
scoped_selectors = selectors.split(",").map do |selector|
|
|
215
|
+
selector = selector.strip
|
|
216
|
+
next selector if selector.empty?
|
|
217
|
+
|
|
218
|
+
if selector.start_with?(":host")
|
|
219
|
+
selector
|
|
220
|
+
.gsub(/^:host-context\(([^)]+)\)/, "#{capsule_attr} \\1")
|
|
221
|
+
.gsub(/^:host\(([^)]+)\)/, "#{capsule_attr}\\1")
|
|
222
|
+
.gsub(/^:host\b/, capsule_attr)
|
|
223
|
+
else
|
|
224
|
+
"#{capsule_attr} #{selector}"
|
|
225
|
+
end
|
|
226
|
+
end.join(", ")
|
|
227
|
+
|
|
228
|
+
leading = selectors_raw[/\A\s*/]
|
|
229
|
+
trailing = selectors_raw[/\s*\z/]
|
|
230
|
+
output << "#{leading}#{scoped_selectors}#{trailing}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
output << "{"
|
|
234
|
+
index += 1
|
|
235
|
+
brace_depth += 1
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
output
|
|
239
|
+
end
|
|
240
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
241
|
+
|
|
242
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity -- splits top-level CSS into hoistable and nestable parts
|
|
243
|
+
def self.hoist_top_level_at_rules(css)
|
|
244
|
+
hoisted = []
|
|
245
|
+
nestable = +""
|
|
246
|
+
index = 0
|
|
247
|
+
length = css.length
|
|
248
|
+
|
|
249
|
+
while index < length
|
|
250
|
+
while index < length && css[index].match?(/\s/)
|
|
251
|
+
nestable << css[index]
|
|
252
|
+
index += 1
|
|
253
|
+
end
|
|
254
|
+
break if index >= length
|
|
255
|
+
|
|
256
|
+
if css[index] == "@"
|
|
257
|
+
at_name = css[index..][/\A@([a-zA-Z-]+)/, 1]&.downcase
|
|
258
|
+
if at_name && HOISTABLE_AT_RULES.include?(at_name)
|
|
259
|
+
block, index = read_top_level_at_rule(css, index)
|
|
260
|
+
hoisted << block.strip
|
|
261
|
+
next
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
rule, index = read_top_level_rule(css, index)
|
|
266
|
+
nestable << rule
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
[hoisted.join("\n"), nestable]
|
|
270
|
+
end
|
|
271
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
272
|
+
|
|
273
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- semicolon vs braced at-rule forms
|
|
274
|
+
def self.read_top_level_at_rule(css, index)
|
|
275
|
+
start_index = index
|
|
276
|
+
length = css.length
|
|
277
|
+
|
|
278
|
+
while index < length && css[index] != "{" && css[index] != ";"
|
|
279
|
+
index += 1
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if index < length && css[index] == ";"
|
|
283
|
+
index += 1
|
|
284
|
+
return [css[start_index...index], index]
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
raise ArgumentError, "Unterminated at-rule in CSS" if index >= length
|
|
288
|
+
|
|
289
|
+
index = consume_braced_block(css, index)
|
|
290
|
+
[css[start_index...index], index]
|
|
291
|
+
end
|
|
292
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
293
|
+
|
|
294
|
+
def self.read_top_level_rule(css, index)
|
|
295
|
+
start_index = index
|
|
296
|
+
length = css.length
|
|
297
|
+
|
|
298
|
+
while index < length && css[index] != "{"
|
|
299
|
+
index += 1
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
return [css[start_index...index], index] if index >= length
|
|
303
|
+
|
|
304
|
+
index = consume_braced_block(css, index)
|
|
305
|
+
[css[start_index...index], index]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# rubocop:disable Metrics/MethodLength -- brace depth walk with unterminated guard
|
|
309
|
+
def self.consume_braced_block(css, index)
|
|
310
|
+
length = css.length
|
|
311
|
+
raise ArgumentError, "Expected opening brace in CSS" unless css[index] == "{"
|
|
312
|
+
|
|
313
|
+
depth = 0
|
|
314
|
+
while index < length
|
|
315
|
+
case css[index]
|
|
316
|
+
when "{"
|
|
317
|
+
depth += 1
|
|
318
|
+
when "}"
|
|
319
|
+
depth -= 1
|
|
320
|
+
index += 1
|
|
321
|
+
return index if depth.zero?
|
|
322
|
+
|
|
323
|
+
next
|
|
324
|
+
end
|
|
325
|
+
index += 1
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
raise ArgumentError, "Unterminated CSS block"
|
|
329
|
+
end
|
|
330
|
+
# rubocop:enable Metrics/MethodLength
|
|
331
|
+
|
|
332
|
+
private_class_method :strip_comments, :validate_capsule_id!, :patch_selectors,
|
|
333
|
+
:hoist_top_level_at_rules, :read_top_level_at_rule, :read_top_level_rule,
|
|
334
|
+
:consume_braced_block
|
|
213
335
|
end
|
|
336
|
+
# rubocop:enable Metrics/ModuleLength
|
|
214
337
|
end
|
|
@@ -61,7 +61,10 @@ module StyleCapsule
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def html_content_type?(content_type)
|
|
64
|
-
|
|
64
|
+
# Core path must not call ActiveSupport String#blank?
|
|
65
|
+
# rubocop:disable Rails/Blank -- standalone hosts may lack ActiveSupport
|
|
66
|
+
return false if content_type.nil? || content_type.empty?
|
|
67
|
+
# rubocop:enable Rails/Blank
|
|
65
68
|
|
|
66
69
|
content_type.split(";", 2).first.to_s.strip.match?(HTML_CONTENT_TYPE)
|
|
67
70
|
end
|
|
@@ -728,10 +728,16 @@ module StyleCapsule
|
|
|
728
728
|
return html if pending_stylesheets.empty?
|
|
729
729
|
|
|
730
730
|
closing_head_index = html.match(%r{</head>}i)&.begin(0)
|
|
731
|
-
|
|
731
|
+
unless closing_head_index
|
|
732
|
+
clear
|
|
733
|
+
return html
|
|
734
|
+
end
|
|
732
735
|
|
|
733
736
|
tags = render_stylesheet_tags(pending_stylesheets, view_context)
|
|
734
|
-
|
|
737
|
+
if tags.empty?
|
|
738
|
+
clear
|
|
739
|
+
return html
|
|
740
|
+
end
|
|
735
741
|
|
|
736
742
|
injected = html.dup
|
|
737
743
|
injected.insert(closing_head_index, "#{tags}\n")
|
|
@@ -167,7 +167,7 @@ module StyleCapsule
|
|
|
167
167
|
|
|
168
168
|
# File caching is only allowed for class method component_styles
|
|
169
169
|
if cache_strategy == :file && !file_caching_allowed?
|
|
170
|
-
|
|
170
|
+
warn_file_cache_fallback(self.class)
|
|
171
171
|
cache_strategy = :none
|
|
172
172
|
cache_ttl = nil
|
|
173
173
|
cache_proc = nil
|
data/sig/style_capsule.rbs
CHANGED
|
@@ -6,9 +6,12 @@ module StyleCapsule
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
module CssProcessor
|
|
9
|
+
MAX_CSS_SIZE: Integer
|
|
10
|
+
NON_SCOPING_AT_RULES: Array[String]
|
|
11
|
+
HOISTABLE_AT_RULES: Array[String]
|
|
12
|
+
|
|
9
13
|
def self.scope_selectors: (String css_string, String capsule_id, ?component_class: Class | String | nil) -> String
|
|
10
14
|
def self.scope_with_nesting: (String css_string, String capsule_id, ?component_class: Class | String | nil) -> String
|
|
11
|
-
def self.strip_comments: (String css) -> String
|
|
12
15
|
def self.reject_style_element_breakout!: (String | nil css_string) -> void
|
|
13
16
|
end
|
|
14
17
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: style_capsule
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -273,11 +273,8 @@ dependencies:
|
|
|
273
273
|
- - "~>"
|
|
274
274
|
- !ruby/object:Gem::Version
|
|
275
275
|
version: '4.0'
|
|
276
|
-
description:
|
|
277
|
-
|
|
278
|
-
scoped to prevent leakage between components. Inspired by component-based CSS approaches
|
|
279
|
-
like Angular's view encapsulation and CSS modules. Works with Rails and can be used
|
|
280
|
-
standalone in other Ruby frameworks (Sinatra, Hanami, etc.) or plain Ruby scripts.
|
|
276
|
+
description: Attribute-based CSS scoping via [data-capsule] for Phlex, ViewComponent,
|
|
277
|
+
and ERB. Works with Rails and standalone in other Ruby frameworks or plain Ruby.
|
|
281
278
|
email:
|
|
282
279
|
- contact@kiskolabs.com
|
|
283
280
|
executables: []
|
|
@@ -290,10 +287,14 @@ files:
|
|
|
290
287
|
- SECURITY.md
|
|
291
288
|
- docs/changelogs/20260904180000_audit-fixes.md
|
|
292
289
|
- docs/changelogs/20260925094000_head-injection-bufferable-chunked.md
|
|
290
|
+
- docs/changelogs/20260928193000_sync-appraisal-locks-after-version-bump.md
|
|
293
291
|
- docs/dependencies/20260904174500_json-cve-2026-71847.md
|
|
294
292
|
- docs/issues/20260904174500_engineering-and-dependency-audit.md
|
|
295
293
|
- docs/issues/20260917123253_ci-drop-automatic-dependency-audit.md
|
|
294
|
+
- docs/issues/20261103150931_engineering-prose-release-audit.md
|
|
296
295
|
- docs/non_rails_support.md
|
|
296
|
+
- docs/reviews/20261028_engineering-audit-release-preparation.md
|
|
297
|
+
- docs/reviews/20261103150931_engineering-prose-release-audit.md
|
|
297
298
|
- lib/style_capsule.rb
|
|
298
299
|
- lib/style_capsule/asset_path.rb
|
|
299
300
|
- lib/style_capsule/class_registry.rb
|