Class: StyleCapsule::StylesheetRegistry
- Inherits:
-
Object
- Object
- StyleCapsule::StylesheetRegistry
- Defined in:
- lib/style_capsule/stylesheet_registry.rb,
sig/style_capsule.rbs
Overview
At runtime inherits Object when ActiveSupport::CurrentAttributes is unavailable.
Constant Summary collapse
- DEFAULT_NAMESPACE =
Default namespace for backward compatibility
:default
Class Attribute Summary collapse
-
.inline_cache ⇒ Object
readonly
Returns the value of attribute inline_cache.
-
.manifest ⇒ Object
readonly
Returns the value of attribute manifest.
Class Method Summary collapse
-
.any?(namespace: nil) ⇒ Boolean
Check if there are any registered stylesheets.
-
.cache_inline_css(cache_key, css_content, cache_strategy:, cache_ttl: nil, cache_proc: nil, capsule_id: nil, namespace: nil) ⇒ void
Cache inline CSS with expiration.
-
.cached_inline(cache_key, cache_strategy:, cache_ttl: nil, cache_proc: nil, css_content: nil, capsule_id: nil, namespace: nil) ⇒ String?
Get cached inline CSS if available and not expired.
-
.cleanup_expired_cache ⇒ Integer
Clean up expired entries from the inline CSS cache.
-
.clear(namespace: nil) ⇒ void
Clear request-scoped inline CSS and render-time file registrations (does not clear process-wide manifest).
-
.clear_inline_cache(cache_key = nil) ⇒ void
Clear inline CSS cache.
-
.clear_manifest(namespace: nil) ⇒ void
Clear process-wide manifest (useful for testing or development reloading).
-
.current_time ⇒ Object
Get current time (ActiveSupport::Time.current or Time.now fallback).
-
.inject_pending_head_stylesheets(html, view_context = nil) ⇒ String
Inject pending request-scoped stylesheets into an HTML document before
</head>. -
.inline_stylesheets ⇒ Object
Get inline stylesheets (thread-local fallback if not using CurrentAttributes).
-
.inline_stylesheets=(value) ⇒ Object
Set inline stylesheets (thread-local fallback if not using CurrentAttributes).
-
.instance ⇒ Object
Get instance (for CurrentAttributes compatibility).
-
.manifest_files ⇒ Hash<Symbol, Array<Hash>>
Get all registered file paths from process-wide manifest (organized by namespace).
-
.normalize_namespace(namespace) ⇒ Symbol
Normalize namespace (nil/blank becomes DEFAULT_NAMESPACE).
-
.pending_head_stylesheets? ⇒ Boolean
Whether request-scoped stylesheets remain to inject into
<head>. -
.register(file_path, namespace: nil, **options) ⇒ void
Register a stylesheet file path during rendering (request-scoped).
-
.register_eager(file_path, namespace: nil, **options) ⇒ void
Register a stylesheet file path eagerly (process-wide manifest).
-
.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) ⇒ void
Register inline CSS for head rendering.
-
.render_head_stylesheets(view_context = nil, namespace: nil) ⇒ String
Render registered stylesheets as HTML.
-
.request_file_stylesheets ⇒ Object
Get request-scoped file stylesheets (thread-local fallback if not using CurrentAttributes).
-
.request_file_stylesheets=(value) ⇒ Object
Set request-scoped file stylesheets (thread-local fallback if not using CurrentAttributes).
-
.request_inline_stylesheets ⇒ Hash<Symbol, Array<Hash>>
Get all registered inline stylesheets for current request (organized by namespace).
-
.request_stylesheet_files ⇒ Hash<Symbol, Hash<String, Hash>>
Get all request-scoped file stylesheets for the current request (organized by namespace).
-
.stylesheets_for(namespace: nil) ⇒ Array<Hash>
Get all stylesheets (files + inline) for a specific namespace.
-
.using_current_attributes? ⇒ Boolean
Check if we're using ActiveSupport::CurrentAttributes This method can be stubbed in tests to test fallback paths.
Class Attribute Details
.inline_cache ⇒ Object (readonly)
Returns the value of attribute inline_cache.
95 96 97 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 95 def inline_cache @inline_cache end |
.manifest ⇒ Object (readonly)
Returns the value of attribute manifest.
95 96 97 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 95 def manifest @manifest end |
Class Method Details
.any?(namespace: nil) ⇒ Boolean
Check if there are any registered stylesheets
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- checks manifest and request-scoped registries
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 625 def self.any?(namespace: nil) if namespace.nil? # Check process-wide manifest has_files = !@manifest.empty? && @manifest.values.any? { |files| !files.empty? } # Check request-scoped file paths request_files = request_file_stylesheets has_request_files = !request_files.empty? && request_files.values.any? { |files| !files.empty? } # Check request-scoped inline CSS inline = request_inline_stylesheets has_inline = !inline.empty? && inline.values.any? { |stylesheets| !stylesheets.empty? } has_files || has_request_files || has_inline else ns = normalize_namespace(namespace) # Check process-wide manifest has_files = @manifest[ns] && !@manifest[ns].empty? # Check request-scoped file paths has_request_files = request_file_stylesheets[ns] && !request_file_stylesheets[ns].empty? # Check request-scoped inline CSS has_inline = request_inline_stylesheets[ns] && !request_inline_stylesheets[ns].empty? !!(has_files || has_request_files || has_inline) end end |
.cache_inline_css(cache_key, css_content, cache_strategy:, cache_ttl: nil, cache_proc: nil, capsule_id: nil, namespace: nil) ⇒ void
This method returns an undefined value.
Cache inline CSS with expiration
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 386 def self.cache_inline_css(cache_key, css_content, cache_strategy:, cache_ttl: nil, cache_proc: nil, capsule_id: nil, namespace: nil) expires_at = nil case cache_strategy when :time # Handle ActiveSupport::Duration (e.g., 1.hour) or integer seconds ttl_seconds = if cache_ttl.respond_to?(:to_i) cache_ttl.to_i else cache_ttl end expires_at = ttl_seconds ? current_time + ttl_seconds : nil when :proc if cache_proc _key, _should_cache, proc_expires = cache_proc.call(css_content, capsule_id, namespace) expires_at = proc_expires end end @inline_cache[cache_key] = { css_content: css_content, cached_at: current_time, expires_at: expires_at } end |
.cached_inline(cache_key, cache_strategy:, cache_ttl: nil, cache_proc: nil, css_content: nil, capsule_id: nil, namespace: nil) ⇒ String?
Get cached inline CSS if available and not expired
Automatically performs lazy cleanup of expired entries if it's been more than 5 minutes since the last cleanup to prevent memory leaks in long-running processes.
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 352 def self.cached_inline(cache_key, cache_strategy:, cache_ttl: nil, cache_proc: nil, css_content: nil, capsule_id: nil, namespace: nil) # Lazy cleanup: remove expired entries if it's been a while (every 5 minutes) # This prevents memory leaks in long-running processes without impacting performance cleanup_expired_cache_if_needed cached_entry = @inline_cache[cache_key] return nil unless cached_entry # Check expiration based on strategy case cache_strategy when :time return nil if cache_ttl && cached_entry[:expires_at] && current_time > cached_entry[:expires_at] when :proc return nil unless cache_proc # Re-validation: the proc is invoked on every read with the *current* request's CSS. # Return [_, true, _] from the proc to keep using the cached entry; false invalidates it. # (This is closer to a conditional cache than a blind key/value store.) _key, should_use, _expires = cache_proc.call(css_content, capsule_id, namespace) return nil unless should_use end cached_entry[:css_content] end |
.cleanup_expired_cache ⇒ Integer
Clean up expired entries from the inline CSS cache
Removes all cache entries that have expired (where expires_at is set and Time.current > expires_at). This prevents memory leaks in long-running processes.
This method is called automatically by cached_inline (lazy cleanup every 5 minutes), but can also be called manually for explicit cleanup (e.g., from a background job or scheduled task).
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 439 def self.cleanup_expired_cache return 0 if @inline_cache.empty? now = current_time expired_keys = [] @inline_cache.each do |cache_key, entry| # Remove entries that have an expires_at time and it's in the past if entry[:expires_at] && now > entry[:expires_at] expired_keys << cache_key end end expired_keys.each { |key| @inline_cache.delete(key) } @last_cleanup_time = now expired_keys.size end |
.clear(namespace: nil) ⇒ void
This method returns an undefined value.
Clear request-scoped inline CSS and render-time file registrations (does not clear process-wide manifest)
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 522 def self.clear(namespace: nil) if namespace.nil? self.inline_stylesheets = {} self.request_file_stylesheets = {} else ns = normalize_namespace(namespace) inline_registry = inline_stylesheets inline_registry.delete(ns) self.inline_stylesheets = inline_registry file_registry = request_file_stylesheets file_registry.delete(ns) self.request_file_stylesheets = file_registry end end |
.clear_inline_cache(cache_key = nil) ⇒ void
This method returns an undefined value.
Clear inline CSS cache
416 417 418 419 420 421 422 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 416 def self.clear_inline_cache(cache_key = nil) if cache_key @inline_cache.delete(cache_key) else @inline_cache.clear end end |
.clear_manifest(namespace: nil) ⇒ void
This method returns an undefined value.
Clear process-wide manifest (useful for testing or development reloading)
542 543 544 545 546 547 548 549 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 542 def self.clear_manifest(namespace: nil) if namespace.nil? @manifest = {} else ns = normalize_namespace(namespace) @manifest.delete(ns) end end |
.current_time ⇒ Object
Get current time (ActiveSupport::Time.current or Time.now fallback)
98 99 100 101 102 103 104 105 106 107 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 98 def current_time if defined?(Time) && Time.respond_to?(:current) Time.current else # rubocop:disable Rails/TimeZone # Time.now is intentional fallback for non-Rails usage when Time.current is unavailable Time.now # rubocop:enable Rails/TimeZone end end |
.inject_pending_head_stylesheets(html, view_context = nil) ⇒ String
Inject pending request-scoped stylesheets into an HTML document before </head>.
Used by HeadInjectionMiddleware after the body has rendered and components have
called register_stylesheet or register_inline.
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 706 def self.inject_pending_head_stylesheets(html, view_context = nil) pending_stylesheets = pending_request_stylesheets return html if pending_stylesheets.empty? closing_head_index = html.match(%r{</head>}i)&.begin(0) return html unless closing_head_index = (pending_stylesheets, view_context) return html if .empty? injected = html.dup injected.insert(closing_head_index, "#{}\n") clear injected end |
.inline_stylesheets ⇒ Object
Get inline stylesheets (thread-local fallback if not using CurrentAttributes)
116 117 118 119 120 121 122 123 124 125 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 116 def inline_stylesheets if using_current_attributes? # When using CurrentAttributes, access the instance attribute # CurrentAttributes automatically provides access to instance attributes inst = instance inst&.inline_stylesheets || {} else Thread.current[:style_capsule_inline_stylesheets] ||= {} end end |
.inline_stylesheets=(value) ⇒ Object
Set inline stylesheets (thread-local fallback if not using CurrentAttributes)
128 129 130 131 132 133 134 135 136 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 128 def inline_stylesheets=(value) if using_current_attributes? # When using CurrentAttributes, set via the instance inst = instance inst.inline_stylesheets = value if inst else Thread.current[:style_capsule_inline_stylesheets] = value end end |
.instance ⇒ Object
Get instance (for CurrentAttributes compatibility)
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 159 def instance if using_current_attributes? # Call the CurrentAttributes instance method from parent class super else # Return a simple object that responds to request-scoped attributes # This is mainly for compatibility with code that might call instance.inline_stylesheets registry_class = self @_standalone_instance ||= begin obj = Object.new obj.define_singleton_method(:inline_stylesheets) { registry_class.inline_stylesheets } obj.define_singleton_method(:inline_stylesheets=) { |v| registry_class.inline_stylesheets = v } obj.define_singleton_method(:request_file_stylesheets) { registry_class.request_file_stylesheets } obj.define_singleton_method(:request_file_stylesheets=) { |v| registry_class.request_file_stylesheets = v } obj end end end |
.manifest_files ⇒ Hash<Symbol, Array<Hash>>
Get all registered file paths from process-wide manifest (organized by namespace)
479 480 481 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 479 def self.manifest_files @manifest.transform_values { |h| h.values } end |
.normalize_namespace(namespace) ⇒ Symbol
Normalize namespace (nil/blank becomes DEFAULT_NAMESPACE)
185 186 187 188 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 185 def self.normalize_namespace(namespace) return DEFAULT_NAMESPACE if namespace.nil? || namespace.to_s.strip.empty? namespace.to_sym end |
.pending_head_stylesheets? ⇒ Boolean
Whether request-scoped stylesheets remain to inject into <head>
514 515 516 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 514 def self.pending_head_stylesheets? !pending_request_stylesheets.empty? end |
.register(file_path, namespace: nil, **options) ⇒ void
This method returns an undefined value.
Register a stylesheet file path during rendering (request-scoped).
Render-time registrations are stored per request and emitted by
render_head_stylesheets when the layout head runs before the body, or by
HeadInjectionMiddleware when components register stylesheets later in the response.
Files registered here are served through Rails asset pipeline (via stylesheet_link_tag).
202 203 204 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 202 def self.register(file_path, namespace: nil, **) register_request_file(file_path, namespace: namespace, **) end |
.register_eager(file_path, namespace: nil, **options) ⇒ void
This method returns an undefined value.
Register a stylesheet file path eagerly (process-wide manifest).
Use at class load or boot time when the stylesheet should always be available in
render_head_stylesheets without waiting for a component render.
215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 215 def self.register_eager(file_path, namespace: nil, **) ns = normalize_namespace(namespace) path = AssetPath.validate_logical_path!(file_path) Instrumentation.instrument_registration( namespace: ns, file_path: path, inline_size: nil, cache_strategy: :none ) do @manifest[ns] ||= {} @manifest[ns][path] = {file_path: path, options: } end end |
.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) ⇒ void
This method returns an undefined value.
Register inline CSS for head rendering
Inline CSS can be cached based on cache configuration. Supports:
- No caching (default): stored per-request
- Time-based caching: cache expires after TTL
- Custom proc caching: use proc to determine cache key and validity
- File-based caching: writes CSS to files for HTTP caching
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- coordinates file, inline, and cache registration
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 269 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) ns = normalize_namespace(namespace) # Handle file-based caching (writes to file and registers as file path) if cache_strategy == :file && component_class && capsule_id # Check if file already exists (from precompilation via assets:precompile or previous write) # Pre-built files are already available through Rails asset pipeline and just need to be registered existing_path = CssFileWriter.file_path_for( component_class: component_class, capsule_id: capsule_id ) if existing_path # File exists (pre-built or previously written), register it as a file path # The manifest deduplicates by logical path if the same file is registered multiple times # This file will be served through Rails asset pipeline (stylesheet_link_tag) = || {} register(existing_path, namespace: namespace, **) return end # File doesn't exist, write it dynamically (development mode or first render) # After writing, register it as a file path so it's served as an asset file_path = CssFileWriter.write_css( css_content: css_content, component_class: component_class, capsule_id: capsule_id ) if file_path # Register as file path instead of inline CSS # This ensures the file is served through Rails asset pipeline = || {} register(file_path, namespace: namespace, **) return end end # Check cache if strategy is enabled cached_css = nil if cache_strategy != :none && cache_key && cache_strategy != :file 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) end # Use cached CSS if available, otherwise use provided CSS final_css = cached_css || css_content Instrumentation.instrument_registration( namespace: ns, file_path: nil, inline_size: final_css.bytesize, cache_strategy: cache_strategy ) do registry = inline_stylesheets registry[ns] ||= [] registry[ns] << { type: :inline, css_content: final_css, capsule_id: capsule_id } self.inline_stylesheets = registry end # Cache the CSS if strategy is enabled and not already cached if cache_strategy != :none && cache_key && !cached_css && cache_strategy != :file 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) end end |
.render_head_stylesheets(view_context = nil, namespace: nil) ⇒ String
Render registered stylesheets as HTML
This should be called in the layout's
section. Combines eager manifest files with request-scoped file paths and inline CSS registered before this call. Stylesheets registered later in the response are injected into<head> by HeadInjectionMiddleware when enabled.
Automatically clears request-scoped inline CSS and file paths after rendering for the selected namespace(s). The eager manifest persists.
rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity -- renders mixed inline and file registrations
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 568 def self.render_head_stylesheets(view_context = nil, namespace: nil) if namespace.nil? || namespace.to_s.strip.empty? request_files_snapshot = request_file_stylesheets.transform_values { |files| files.keys.dup } request_inline_snapshot = request_inline_stylesheets.transform_values(&:dup) all_stylesheets = merged_file_registrations_all_namespaces request_inline_stylesheets.each do |_ns, inline| all_stylesheets.concat(inline) end return safe_string("") if all_stylesheets.empty? rendered = all_stylesheets.map do |stylesheet| if stylesheet[:type] == :inline render_inline_stylesheet(stylesheet, view_context) else render_file_stylesheet(stylesheet, view_context) end end.join("\n").then { |s| safe_string(s) } clear_snapshotted_request_registrations(request_files_snapshot, request_inline_snapshot) rendered else # Render specific namespace ns = normalize_namespace(namespace) snapshotted_file_paths = request_file_stylesheets[ns]&.keys&.dup || [] snapshotted_inline = request_inline_stylesheets[ns]&.dup || [] stylesheets = stylesheets_for(namespace: ns).dup return safe_string("") if stylesheets.empty? rendered = stylesheets.map do |stylesheet| if stylesheet[:type] == :inline render_inline_stylesheet(stylesheet, view_context) else render_file_stylesheet(stylesheet, view_context) end end.join("\n").then { |s| safe_string(s) } clear_snapshotted_request_registrations_for_namespace( ns, file_paths: snapshotted_file_paths, snapshotted_inline_stylesheets: snapshotted_inline ) rendered end end |
.request_file_stylesheets ⇒ Object
Get request-scoped file stylesheets (thread-local fallback if not using CurrentAttributes)
139 140 141 142 143 144 145 146 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 139 def request_file_stylesheets if using_current_attributes? inst = instance inst&.request_file_stylesheets || {} else Thread.current[:style_capsule_request_file_stylesheets] ||= {} end end |
.request_file_stylesheets=(value) ⇒ Object
Set request-scoped file stylesheets (thread-local fallback if not using CurrentAttributes)
149 150 151 152 153 154 155 156 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 149 def request_file_stylesheets=(value) if using_current_attributes? inst = instance inst.request_file_stylesheets = value if inst else Thread.current[:style_capsule_request_file_stylesheets] = value end end |
.request_inline_stylesheets ⇒ Hash<Symbol, Array<Hash>>
Get all registered inline stylesheets for current request (organized by namespace)
486 487 488 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 486 def self.request_inline_stylesheets inline_stylesheets end |
.request_stylesheet_files ⇒ Hash<Symbol, Hash<String, Hash>>
Get all request-scoped file stylesheets for the current request (organized by namespace)
493 494 495 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 493 def self.request_stylesheet_files request_file_stylesheets end |
.stylesheets_for(namespace: nil) ⇒ Array<Hash>
Get all stylesheets (files + inline) for a specific namespace
501 502 503 504 505 506 507 508 509 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 501 def self.stylesheets_for(namespace: nil) ns = normalize_namespace(namespace) result = merged_file_registrations_for_namespace(ns) inline = request_inline_stylesheets[ns] || [] result.concat(inline) result end |
.using_current_attributes? ⇒ Boolean
Check if we're using ActiveSupport::CurrentAttributes This method can be stubbed in tests to test fallback paths
111 112 113 |
# File 'lib/style_capsule/stylesheet_registry.rb', line 111 def using_current_attributes? defined?(ActiveSupport::CurrentAttributes) && self < ActiveSupport::CurrentAttributes end |