style_capsule 1.0.2

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.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StyleCapsule
4
+ # Railtie to automatically include StyleCapsule helpers in Rails
5
+ if defined?(Rails::Railtie)
6
+ class Railtie < Rails::Railtie
7
+ # Automatically include ERB helper in ActionView::Base (standard Rails pattern)
8
+ # This makes helpers available in all ERB templates automatically
9
+ ActiveSupport.on_load(:action_view) do
10
+ include StyleCapsule::Helper
11
+ end
12
+
13
+ # Configure CSS file writer for file-based caching
14
+ config.after_initialize do
15
+ # Configure default output directory
16
+ StyleCapsule::CssFileWriter.configure(
17
+ output_dir: Rails.root.join(StyleCapsule::CssFileWriter::DEFAULT_OUTPUT_DIR),
18
+ enabled: true
19
+ )
20
+
21
+ # Add output directory to asset paths if it exists
22
+ if Rails.application.config.respond_to?(:assets)
23
+ capsules_dir = Rails.root.join(StyleCapsule::CssFileWriter::DEFAULT_OUTPUT_DIR)
24
+ if Dir.exist?(capsules_dir)
25
+ Rails.application.config.assets.paths << capsules_dir
26
+ end
27
+ end
28
+ end
29
+
30
+ # Clear CSS caches and stylesheet manifest when classes are unloaded in development
31
+ # This prevents memory leaks from stale cache entries during code reloading
32
+ if Rails.env.development?
33
+ config.to_prepare do
34
+ # Clear CSS caches for all classes that include StyleCapsule modules
35
+ # Skip singleton classes and handle errors gracefully
36
+ ObjectSpace.each_object(Class) do |klass|
37
+ # Skip singleton classes and classes without names (they can cause errors)
38
+ # Singleton classes don't have proper names and can't be safely checked
39
+ next if klass.name.blank?
40
+ next if klass.singleton_class?
41
+
42
+ # Use method_defined? instead of respond_to? to avoid triggering delegation
43
+ # that might cause errors with singleton classes or other edge cases
44
+ begin
45
+ if klass.method_defined?(:clear_css_cache, false) || klass.private_method_defined?(:clear_css_cache)
46
+ klass.clear_css_cache
47
+ end
48
+ rescue
49
+ # Skip classes that cause errors (singleton classes, delegation issues, etc.)
50
+ next
51
+ end
52
+ end
53
+
54
+ # Clear stylesheet manifest to allow re-registration during code reload
55
+ StyleCapsule::StylesheetRegistry.clear_manifest
56
+ end
57
+ end
58
+
59
+ # Load rake tasks for CSS file building
60
+ rake_tasks do
61
+ load File.expand_path("../tasks/style_capsule.rake", __dir__)
62
+ end
63
+
64
+ # Note: PhlexHelper should be included explicitly in ApplicationComponent
65
+ # or your base Phlex component class, not automatically via Railtie
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,494 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/current_attributes"
4
+
5
+ module StyleCapsule
6
+ # Hybrid registry for stylesheet files that need to be injected into <head>
7
+ #
8
+ # Uses a process-wide manifest for static file paths (like Propshaft) and request-scoped
9
+ # storage for inline CSS. This approach:
10
+ # - Collects static file paths once per process (no rebuilding on each request)
11
+ # - Stores inline CSS per-request (since it's component-specific)
12
+ # - Works correctly with both threaded and forked web servers (Puma, Unicorn, etc.)
13
+ #
14
+ # Supports namespaces for separation of stylesheets (e.g., "admin", "user", "public").
15
+ #
16
+ # @example Usage in a component with head rendering
17
+ # class MyComponent < ApplicationComponent
18
+ # include StyleCapsule::Component
19
+ # stylesheet_registry # Enable head rendering
20
+ #
21
+ # def component_styles
22
+ # <<~CSS
23
+ # .section { color: red; }
24
+ # CSS
25
+ # end
26
+ # end
27
+ #
28
+ # @example Register a stylesheet file manually (default namespace)
29
+ # StyleCapsule::StylesheetRegistry.register('stylesheets/my_component')
30
+ #
31
+ # @example Register a stylesheet with a namespace
32
+ # StyleCapsule::StylesheetRegistry.register('stylesheets/admin/dashboard', namespace: :admin)
33
+ #
34
+ # @example Usage in layout (ERB) - render all namespaces
35
+ # # In app/views/layouts/application.html.erb
36
+ # <head>
37
+ # <%= StyleCapsule::StylesheetRegistry.render_head_stylesheets(self) %>
38
+ # </head>
39
+ #
40
+ # @example Usage in layout (ERB) - render specific namespace
41
+ # <head>
42
+ # <%= StyleCapsule::StylesheetRegistry.render_head_stylesheets(self, namespace: :admin) %>
43
+ # </head>
44
+ #
45
+ # @example Usage in layout (Phlex)
46
+ # # In app/views/layouts/application_layout.rb
47
+ # def view_template(&block)
48
+ # html do
49
+ # head do
50
+ # raw StyleCapsule::StylesheetRegistry.render_head_stylesheets(view_context)
51
+ # end
52
+ # body(&block)
53
+ # end
54
+ # end
55
+ class StylesheetRegistry < ActiveSupport::CurrentAttributes
56
+ # Default namespace for backward compatibility
57
+ DEFAULT_NAMESPACE = :default
58
+
59
+ # Process-wide manifest for static file paths (like Propshaft)
60
+ # Organized by namespace: { namespace => Set of {file_path, options} hashes }
61
+ @manifest = {} # rubocop:disable Style/ClassVars
62
+
63
+ # Process-wide cache for inline CSS (with expiration support)
64
+ # Structure: { cache_key => { css_content: String, cached_at: Time, expires_at: Time } }
65
+ @inline_cache = {} # rubocop:disable Style/ClassVars
66
+
67
+ # Track last cleanup time for lazy cleanup (prevents excessive cleanup calls)
68
+ @last_cleanup_time = nil # rubocop:disable Style/ClassVars
69
+
70
+ # Request-scoped storage for inline CSS only
71
+ attribute :inline_stylesheets
72
+
73
+ class << self
74
+ attr_reader :manifest, :inline_cache
75
+ end
76
+
77
+ # Normalize namespace (nil/blank becomes DEFAULT_NAMESPACE)
78
+ #
79
+ # @param namespace [Symbol, String, nil] Namespace identifier
80
+ # @return [Symbol] Normalized namespace
81
+ def self.normalize_namespace(namespace)
82
+ return DEFAULT_NAMESPACE if namespace.nil? || namespace.to_s.strip.empty?
83
+ namespace.to_sym
84
+ end
85
+
86
+ # Register a stylesheet file path for head rendering
87
+ #
88
+ # Static file paths are stored in process-wide manifest (collected once per process).
89
+ # This is similar to Propshaft's manifest approach - files are static, so we can
90
+ # collect them process-wide without rebuilding on each request.
91
+ #
92
+ # Files registered here are served through Rails asset pipeline (via stylesheet_link_tag).
93
+ # This includes both:
94
+ # - Pre-built files from assets:precompile (already in asset pipeline)
95
+ # - Dynamically written files (written during runtime, also served through asset pipeline)
96
+ #
97
+ # The Set automatically deduplicates entries, so registering the same file multiple times
98
+ # (e.g., when the same component renders multiple times) is safe and efficient.
99
+ #
100
+ # @param file_path [String] Path to stylesheet (relative to app/assets/stylesheets)
101
+ # @param namespace [Symbol, String, nil] Optional namespace for separation (nil/blank uses default)
102
+ # @param options [Hash] Options for stylesheet_link_tag
103
+ # @return [void]
104
+ def self.register(file_path, namespace: nil, **options)
105
+ ns = normalize_namespace(namespace)
106
+ @manifest[ns] ||= Set.new
107
+ # Use a hash with file_path and options as the key to avoid duplicates
108
+ # Set will automatically deduplicate based on hash equality
109
+ entry = {file_path: file_path, options: options}
110
+ @manifest[ns] << entry
111
+ end
112
+
113
+ # Register inline CSS for head rendering
114
+ #
115
+ # Inline CSS can be cached based on cache configuration. Supports:
116
+ # - No caching (default): stored per-request
117
+ # - Time-based caching: cache expires after TTL
118
+ # - Custom proc caching: use proc to determine cache key and validity
119
+ # - File-based caching: writes CSS to files for HTTP caching
120
+ #
121
+ # @param css_content [String] CSS content (should already be scoped)
122
+ # @param namespace [Symbol, String, nil] Optional namespace for separation (nil/blank uses default)
123
+ # @param capsule_id [String, nil] Optional capsule ID for reference
124
+ # @param cache_key [String, nil] Optional cache key (for cache lookup)
125
+ # @param cache_strategy [Symbol, nil] Cache strategy: :none, :time, :proc, :file (default: :none)
126
+ # @param cache_ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in seconds (for :time strategy). Supports ActiveSupport::Duration (e.g., 1.hour, 30.minutes)
127
+ # @param cache_proc [Proc, nil] Custom cache proc (for :proc strategy)
128
+ # Proc receives: (css_content, capsule_id, namespace) and should return [cache_key, should_cache, expires_at]
129
+ # @param component_class [Class, nil] Component class (for :file strategy)
130
+ # @param stylesheet_link_options [Hash, nil] Options for stylesheet_link_tag (for :file strategy)
131
+ # @return [void]
132
+ def self.register_inline(css_content, namespace: nil, capsule_id: nil, cache_key: nil, cache_strategy: :none, cache_ttl: nil, cache_proc: nil, component_class: nil, stylesheet_link_options: nil)
133
+ ns = normalize_namespace(namespace)
134
+
135
+ # Handle file-based caching (writes to file and registers as file path)
136
+ if cache_strategy == :file && component_class && capsule_id
137
+ # Check if file already exists (from precompilation via assets:precompile or previous write)
138
+ # Pre-built files are already available through Rails asset pipeline and just need to be registered
139
+ existing_path = CssFileWriter.file_path_for(
140
+ component_class: component_class,
141
+ capsule_id: capsule_id
142
+ )
143
+
144
+ if existing_path
145
+ # File exists (pre-built or previously written), register it as a file path
146
+ # The Set in @manifest will deduplicate if the same file is registered multiple times
147
+ # This file will be served through Rails asset pipeline (stylesheet_link_tag)
148
+ link_options = stylesheet_link_options || {}
149
+ register(existing_path, namespace: namespace, **link_options)
150
+ return
151
+ end
152
+
153
+ # File doesn't exist, write it dynamically (development mode or first render)
154
+ # After writing, register it as a file path so it's served as an asset
155
+ file_path = CssFileWriter.write_css(
156
+ css_content: css_content,
157
+ component_class: component_class,
158
+ capsule_id: capsule_id
159
+ )
160
+
161
+ if file_path
162
+ # Register as file path instead of inline CSS
163
+ # This ensures the file is served through Rails asset pipeline
164
+ link_options = stylesheet_link_options || {}
165
+ register(file_path, namespace: namespace, **link_options)
166
+ return
167
+ end
168
+ end
169
+
170
+ # Check cache if strategy is enabled
171
+ cached_css = nil
172
+ if cache_strategy != :none && cache_key && cache_strategy != :file
173
+ cached_css = cached_inline(cache_key, cache_strategy: cache_strategy, cache_ttl: cache_ttl, cache_proc: cache_proc, css_content: css_content, capsule_id: capsule_id, namespace: ns)
174
+ end
175
+
176
+ # Use cached CSS if available, otherwise use provided CSS
177
+ final_css = cached_css || css_content
178
+
179
+ # Store in request-scoped registry
180
+ registry = instance.inline_stylesheets || {}
181
+ registry[ns] ||= []
182
+ registry[ns] << {
183
+ type: :inline,
184
+ css_content: final_css,
185
+ capsule_id: capsule_id
186
+ }
187
+ instance.inline_stylesheets = registry
188
+
189
+ # Cache the CSS if strategy is enabled and not already cached
190
+ if cache_strategy != :none && cache_key && !cached_css && cache_strategy != :file
191
+ cache_inline_css(cache_key, css_content, cache_strategy: cache_strategy, cache_ttl: cache_ttl, cache_proc: cache_proc, capsule_id: capsule_id, namespace: ns)
192
+ end
193
+ end
194
+
195
+ # Get cached inline CSS if available and not expired
196
+ #
197
+ # Automatically performs lazy cleanup of expired entries if it's been more than
198
+ # 5 minutes since the last cleanup to prevent memory leaks in long-running processes.
199
+ #
200
+ # @param cache_key [String] Cache key to look up
201
+ # @param cache_strategy [Symbol] Cache strategy
202
+ # @param cache_ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in seconds. Supports ActiveSupport::Duration (e.g., 1.hour, 30.minutes)
203
+ # @param cache_proc [Proc, nil] Custom cache proc
204
+ # @param css_content [String] Original CSS content (for proc strategy)
205
+ # @param capsule_id [String, nil] Capsule ID (for proc strategy)
206
+ # @param namespace [Symbol] Namespace (for proc strategy)
207
+ # @return [String, nil] Cached CSS content or nil if not cached/expired
208
+ def self.cached_inline(cache_key, cache_strategy:, cache_ttl: nil, cache_proc: nil, css_content: nil, capsule_id: nil, namespace: nil)
209
+ # Lazy cleanup: remove expired entries if it's been a while (every 5 minutes)
210
+ # This prevents memory leaks in long-running processes without impacting performance
211
+ cleanup_expired_cache_if_needed
212
+
213
+ cached_entry = @inline_cache[cache_key]
214
+ return nil unless cached_entry
215
+
216
+ # Check expiration based on strategy
217
+ case cache_strategy
218
+ when :time
219
+ return nil if cache_ttl && cached_entry[:expires_at] && Time.current > cached_entry[:expires_at]
220
+ when :proc
221
+ return nil unless cache_proc
222
+ # Proc should validate cache entry
223
+ _key, should_use, _expires = cache_proc.call(css_content, capsule_id, namespace)
224
+ return nil unless should_use
225
+ end
226
+
227
+ cached_entry[:css_content]
228
+ end
229
+
230
+ # Cache inline CSS with expiration
231
+ #
232
+ # @param cache_key [String] Cache key
233
+ # @param css_content [String] CSS content to cache
234
+ # @param cache_strategy [Symbol] Cache strategy
235
+ # @param cache_ttl [Integer, ActiveSupport::Duration, nil] Time-to-live in seconds. Supports ActiveSupport::Duration (e.g., 1.hour, 30.minutes)
236
+ # @param cache_proc [Proc, nil] Custom cache proc
237
+ # @param capsule_id [String, nil] Capsule ID
238
+ # @param namespace [Symbol] Namespace
239
+ # @return [void]
240
+ def self.cache_inline_css(cache_key, css_content, cache_strategy:, cache_ttl: nil, cache_proc: nil, capsule_id: nil, namespace: nil)
241
+ expires_at = nil
242
+
243
+ case cache_strategy
244
+ when :time
245
+ expires_at = cache_ttl ? Time.current + cache_ttl : nil
246
+ when :proc
247
+ if cache_proc
248
+ _key, _should_cache, proc_expires = cache_proc.call(css_content, capsule_id, namespace)
249
+ expires_at = proc_expires
250
+ end
251
+ end
252
+
253
+ @inline_cache[cache_key] = {
254
+ css_content: css_content,
255
+ cached_at: Time.current,
256
+ expires_at: expires_at
257
+ }
258
+ end
259
+
260
+ # Clear inline CSS cache
261
+ #
262
+ # @param cache_key [String, nil] Specific cache key to clear (nil clears all)
263
+ # @return [void]
264
+ def self.clear_inline_cache(cache_key = nil)
265
+ if cache_key
266
+ @inline_cache.delete(cache_key)
267
+ else
268
+ @inline_cache.clear
269
+ end
270
+ end
271
+
272
+ # Clean up expired entries from the inline CSS cache
273
+ #
274
+ # Removes all cache entries that have expired (where expires_at is set and
275
+ # Time.current > expires_at). This prevents memory leaks in long-running processes.
276
+ #
277
+ # This method is called automatically by cached_inline (lazy cleanup every 5 minutes),
278
+ # but can also be called manually for explicit cleanup (e.g., from a background job
279
+ # or scheduled task).
280
+ #
281
+ # @return [Integer] Number of expired entries removed
282
+ # @example Manual cleanup
283
+ # StyleCapsule::StylesheetRegistry.cleanup_expired_cache
284
+ # @example Scheduled cleanup (e.g., in a background job)
285
+ # # In a scheduled job or initializer
286
+ # StyleCapsule::StylesheetRegistry.cleanup_expired_cache
287
+ def self.cleanup_expired_cache
288
+ return 0 if @inline_cache.empty?
289
+
290
+ current_time = Time.current
291
+ expired_keys = []
292
+
293
+ @inline_cache.each do |cache_key, entry|
294
+ # Remove entries that have an expires_at time and it's in the past
295
+ if entry[:expires_at] && current_time > entry[:expires_at]
296
+ expired_keys << cache_key
297
+ end
298
+ end
299
+
300
+ expired_keys.each { |key| @inline_cache.delete(key) }
301
+ @last_cleanup_time = current_time
302
+
303
+ expired_keys.size
304
+ end
305
+
306
+ # Check if cleanup is needed and perform it if so
307
+ #
308
+ # Only performs cleanup if it's been more than 5 minutes since the last cleanup.
309
+ # This prevents excessive cleanup calls while still preventing memory leaks.
310
+ #
311
+ # @return [void]
312
+ # @api private
313
+ def self.cleanup_expired_cache_if_needed
314
+ # Cleanup every 5 minutes (300 seconds) to balance memory usage and performance
315
+ cleanup_interval = 300
316
+
317
+ if @last_cleanup_time.nil? || (Time.current - @last_cleanup_time) > cleanup_interval
318
+ cleanup_expired_cache
319
+ end
320
+ end
321
+
322
+ private_class_method :cleanup_expired_cache_if_needed
323
+
324
+ # Get all registered file paths from process-wide manifest (organized by namespace)
325
+ #
326
+ # @return [Hash<Symbol, Set<Hash>>] Hash of namespace => set of file registrations
327
+ def self.manifest_files
328
+ @manifest.dup
329
+ end
330
+
331
+ # Get all registered inline stylesheets for current request (organized by namespace)
332
+ #
333
+ # @return [Hash<Symbol, Array<Hash>>] Hash of namespace => array of inline stylesheet registrations
334
+ def self.request_inline_stylesheets
335
+ instance.inline_stylesheets || {}
336
+ end
337
+
338
+ # Get all stylesheets (files + inline) for a specific namespace
339
+ #
340
+ # @param namespace [Symbol, String, nil] Namespace identifier (nil/blank uses default)
341
+ # @return [Array<Hash>] Array of stylesheet registrations for the namespace
342
+ def self.stylesheets_for(namespace: nil)
343
+ ns = normalize_namespace(namespace)
344
+ result = []
345
+
346
+ # Add process-wide file paths
347
+ if @manifest[ns]
348
+ result.concat(@manifest[ns].to_a)
349
+ end
350
+
351
+ # Add request-scoped inline CSS
352
+ inline = request_inline_stylesheets[ns] || []
353
+ result.concat(inline)
354
+
355
+ result
356
+ end
357
+
358
+ # Clear request-scoped inline stylesheets (does not clear process-wide manifest)
359
+ #
360
+ # @param namespace [Symbol, String, nil] Optional namespace to clear (nil clears all)
361
+ # @return [void]
362
+ def self.clear(namespace: nil)
363
+ if namespace.nil?
364
+ instance.inline_stylesheets = {}
365
+ else
366
+ ns = normalize_namespace(namespace)
367
+ registry = instance.inline_stylesheets || {}
368
+ registry.delete(ns)
369
+ instance.inline_stylesheets = registry
370
+ end
371
+ end
372
+
373
+ # Clear process-wide manifest (useful for testing or development reloading)
374
+ #
375
+ # @param namespace [Symbol, String, nil] Optional namespace to clear (nil clears all)
376
+ # @return [void]
377
+ def self.clear_manifest(namespace: nil)
378
+ if namespace.nil?
379
+ @manifest = {}
380
+ else
381
+ ns = normalize_namespace(namespace)
382
+ @manifest.delete(ns)
383
+ end
384
+ end
385
+
386
+ # Render registered stylesheets as HTML
387
+ #
388
+ # This should be called in the layout's <head> section.
389
+ # Combines process-wide file manifest with request-scoped inline CSS.
390
+ # Automatically clears request-scoped inline CSS after rendering (manifest persists).
391
+ #
392
+ # @param view_context [ActionView::Base, nil] The view context (for helpers like content_tag, stylesheet_link_tag)
393
+ # In ERB: pass `self` (the view context)
394
+ # In Phlex: pass `view_context` method
395
+ # If nil, falls back to basic HTML generation
396
+ # @param namespace [Symbol, String, nil] Optional namespace to render (nil/blank renders all namespaces)
397
+ # @return [String] HTML-safe string with stylesheet tags
398
+ def self.render_head_stylesheets(view_context = nil, namespace: nil)
399
+ if namespace.nil? || namespace.to_s.strip.empty?
400
+ # Render all namespaces
401
+ all_stylesheets = []
402
+
403
+ # Collect from process-wide manifest (all namespaces)
404
+ @manifest.each do |ns, files|
405
+ all_stylesheets.concat(files.to_a)
406
+ end
407
+
408
+ # Collect from request-scoped inline CSS (all namespaces)
409
+ request_inline_stylesheets.each do |_ns, inline|
410
+ all_stylesheets.concat(inline)
411
+ end
412
+
413
+ clear # Clear request-scoped inline CSS only
414
+ return "".html_safe if all_stylesheets.empty?
415
+
416
+ all_stylesheets.map do |stylesheet|
417
+ if stylesheet[:type] == :inline
418
+ render_inline_stylesheet(stylesheet, view_context)
419
+ else
420
+ render_file_stylesheet(stylesheet, view_context)
421
+ end
422
+ end.join("\n").html_safe
423
+
424
+ else
425
+ # Render specific namespace
426
+ ns = normalize_namespace(namespace)
427
+ stylesheets = stylesheets_for(namespace: ns).dup
428
+ clear(namespace: ns) # Clear request-scoped inline CSS only
429
+
430
+ return "".html_safe if stylesheets.empty?
431
+
432
+ stylesheets.map do |stylesheet|
433
+ if stylesheet[:type] == :inline
434
+ render_inline_stylesheet(stylesheet, view_context)
435
+ else
436
+ render_file_stylesheet(stylesheet, view_context)
437
+ end
438
+ end.join("\n").html_safe
439
+
440
+ end
441
+ end
442
+
443
+ # Check if there are any registered stylesheets
444
+ #
445
+ # @param namespace [Symbol, String, nil] Optional namespace to check (nil checks all)
446
+ # @return [Boolean]
447
+ def self.any?(namespace: nil)
448
+ if namespace.nil?
449
+ # Check process-wide manifest
450
+ has_files = !@manifest.empty? && @manifest.values.any? { |files| !files.empty? }
451
+ # Check request-scoped inline CSS
452
+ inline = request_inline_stylesheets
453
+ has_inline = !inline.empty? && inline.values.any? { |stylesheets| !stylesheets.empty? }
454
+ has_files || has_inline
455
+ else
456
+ ns = normalize_namespace(namespace)
457
+ # Check process-wide manifest
458
+ has_files = @manifest[ns] && !@manifest[ns].empty?
459
+ # Check request-scoped inline CSS
460
+ has_inline = request_inline_stylesheets[ns] && !request_inline_stylesheets[ns].empty?
461
+ !!(has_files || has_inline)
462
+ end
463
+ end
464
+
465
+ # Render a file-based stylesheet
466
+ def self.render_file_stylesheet(stylesheet, view_context)
467
+ file_path = stylesheet[:file_path]
468
+ options = stylesheet[:options] || {}
469
+
470
+ if view_context&.respond_to?(:stylesheet_link_tag)
471
+ view_context.stylesheet_link_tag(file_path, **options)
472
+ else
473
+ # Fallback if no view context
474
+ href = "/assets/#{file_path}.css"
475
+ tag_options = options.map { |k, v| %(#{k}="#{v}") }.join(" ")
476
+ %(<link rel="stylesheet" href="#{href}"#{" #{tag_options}" unless tag_options.empty?}>).html_safe
477
+ end
478
+ end
479
+
480
+ # Render an inline stylesheet
481
+ def self.render_inline_stylesheet(stylesheet, view_context)
482
+ # CSS content is already scoped when registered from components
483
+ # capsule_id is stored for reference but CSS is pre-processed
484
+ css_content = stylesheet[:css_content]
485
+
486
+ # Construct HTML manually to avoid any HTML escaping issues
487
+ # CSS content should not be HTML-escaped as it's inside a <style> tag
488
+ # Using string interpolation with html_safe ensures CSS is not escaped
489
+ %(<style type="text/css">#{css_content}</style>).html_safe
490
+ end
491
+
492
+ private_class_method :render_file_stylesheet, :render_inline_stylesheet
493
+ end
494
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StyleCapsule
4
+ VERSION = "1.0.2"
5
+ end