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
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "instrumentation"
|
|
4
|
+
require_relative "asset_path"
|
|
5
|
+
|
|
3
6
|
module StyleCapsule
|
|
4
7
|
# Helper to determine the parent class for StylesheetRegistry
|
|
5
8
|
# ActiveSupport::CurrentAttributes is optional - if ActiveSupport is loaded,
|
|
@@ -13,10 +16,13 @@ module StyleCapsule
|
|
|
13
16
|
|
|
14
17
|
# Hybrid registry for stylesheet files that need to be injected into <head>
|
|
15
18
|
#
|
|
16
|
-
# Uses a process-wide manifest for
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
19
|
+
# Uses a process-wide manifest for eager registrations, request-scoped storage for
|
|
20
|
+
# render-time file paths and inline CSS, and optional Rack middleware to inject
|
|
21
|
+
# stylesheets registered during body rendering into +<head>+ on the same request.
|
|
22
|
+
#
|
|
23
|
+
# This approach:
|
|
24
|
+
# - Keeps eager file paths in a process-wide manifest (class load / boot time)
|
|
25
|
+
# - Stores render-time file paths and inline CSS per request
|
|
20
26
|
# - Works correctly with both threaded and forked web servers (Puma, Unicorn, etc.)
|
|
21
27
|
#
|
|
22
28
|
# Supports namespaces for separation of stylesheets (e.g., "admin", "user", "public").
|
|
@@ -33,9 +39,12 @@ module StyleCapsule
|
|
|
33
39
|
# end
|
|
34
40
|
# end
|
|
35
41
|
#
|
|
36
|
-
# @example Register a stylesheet
|
|
42
|
+
# @example Register a stylesheet during rendering (request-scoped)
|
|
37
43
|
# StyleCapsule::StylesheetRegistry.register('stylesheets/my_component')
|
|
38
44
|
#
|
|
45
|
+
# @example Register a stylesheet eagerly at boot or class load (process-wide manifest)
|
|
46
|
+
# StyleCapsule::StylesheetRegistry.register_eager('stylesheets/my_component', namespace: :user)
|
|
47
|
+
#
|
|
39
48
|
# @example Register a stylesheet with a namespace
|
|
40
49
|
# StyleCapsule::StylesheetRegistry.register('stylesheets/admin/dashboard', namespace: :admin)
|
|
41
50
|
#
|
|
@@ -65,20 +74,21 @@ module StyleCapsule
|
|
|
65
74
|
DEFAULT_NAMESPACE = :default
|
|
66
75
|
|
|
67
76
|
# Process-wide manifest for static file paths (like Propshaft)
|
|
68
|
-
# Organized by namespace: { namespace =>
|
|
69
|
-
@manifest = {} # rubocop:disable Style/ClassVars
|
|
77
|
+
# Organized by namespace: { namespace => { logical_path => { file_path:, options: } } }
|
|
78
|
+
@manifest = {} # rubocop:disable Style/ClassVars, ThreadSafety/MutableClassInstanceVariable
|
|
70
79
|
|
|
71
80
|
# Process-wide cache for inline CSS (with expiration support)
|
|
72
81
|
# Structure: { cache_key => { css_content: String, cached_at: Time, expires_at: Time } }
|
|
73
|
-
@inline_cache = {} # rubocop:disable Style/ClassVars
|
|
82
|
+
@inline_cache = {} # rubocop:disable Style/ClassVars, ThreadSafety/MutableClassInstanceVariable
|
|
74
83
|
|
|
75
84
|
# Track last cleanup time for lazy cleanup (prevents excessive cleanup calls)
|
|
76
85
|
@last_cleanup_time = nil # rubocop:disable Style/ClassVars
|
|
77
86
|
|
|
78
|
-
# Request-scoped storage for inline CSS
|
|
87
|
+
# Request-scoped storage for inline CSS and render-time file registrations
|
|
79
88
|
# Only define attribute if we're inheriting from CurrentAttributes
|
|
80
89
|
if defined?(ActiveSupport::CurrentAttributes) && self < ActiveSupport::CurrentAttributes
|
|
81
90
|
attribute :inline_stylesheets
|
|
91
|
+
attribute :request_file_stylesheets
|
|
82
92
|
end
|
|
83
93
|
|
|
84
94
|
class << self
|
|
@@ -125,19 +135,41 @@ module StyleCapsule
|
|
|
125
135
|
end
|
|
126
136
|
end
|
|
127
137
|
|
|
138
|
+
# Get request-scoped file stylesheets (thread-local fallback if not using CurrentAttributes)
|
|
139
|
+
def request_file_stylesheets
|
|
140
|
+
if using_current_attributes?
|
|
141
|
+
inst = instance
|
|
142
|
+
inst&.request_file_stylesheets || {}
|
|
143
|
+
else
|
|
144
|
+
Thread.current[:style_capsule_request_file_stylesheets] ||= {}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Set request-scoped file stylesheets (thread-local fallback if not using CurrentAttributes)
|
|
149
|
+
def request_file_stylesheets=(value)
|
|
150
|
+
if using_current_attributes?
|
|
151
|
+
inst = instance
|
|
152
|
+
inst.request_file_stylesheets = value if inst
|
|
153
|
+
else
|
|
154
|
+
Thread.current[:style_capsule_request_file_stylesheets] = value
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
128
158
|
# Get instance (for CurrentAttributes compatibility)
|
|
129
159
|
def instance
|
|
130
160
|
if using_current_attributes?
|
|
131
161
|
# Call the CurrentAttributes instance method from parent class
|
|
132
162
|
super
|
|
133
163
|
else
|
|
134
|
-
# Return a simple object that responds to
|
|
164
|
+
# Return a simple object that responds to request-scoped attributes
|
|
135
165
|
# This is mainly for compatibility with code that might call instance.inline_stylesheets
|
|
136
166
|
registry_class = self
|
|
137
167
|
@_standalone_instance ||= begin
|
|
138
168
|
obj = Object.new
|
|
139
169
|
obj.define_singleton_method(:inline_stylesheets) { registry_class.inline_stylesheets }
|
|
140
170
|
obj.define_singleton_method(:inline_stylesheets=) { |v| registry_class.inline_stylesheets = v }
|
|
171
|
+
obj.define_singleton_method(:request_file_stylesheets) { registry_class.request_file_stylesheets }
|
|
172
|
+
obj.define_singleton_method(:request_file_stylesheets=) { |v| registry_class.request_file_stylesheets = v }
|
|
141
173
|
obj
|
|
142
174
|
end
|
|
143
175
|
end
|
|
@@ -155,33 +187,65 @@ module StyleCapsule
|
|
|
155
187
|
namespace.to_sym
|
|
156
188
|
end
|
|
157
189
|
|
|
158
|
-
# Register a stylesheet file path
|
|
190
|
+
# Register a stylesheet file path during rendering (request-scoped).
|
|
159
191
|
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
192
|
+
# Render-time registrations are stored per request and emitted by
|
|
193
|
+
# +render_head_stylesheets+ when the layout head runs before the body, or by
|
|
194
|
+
# +HeadInjectionMiddleware+ when components register stylesheets later in the response.
|
|
163
195
|
#
|
|
164
196
|
# Files registered here are served through Rails asset pipeline (via stylesheet_link_tag).
|
|
165
|
-
# This includes both:
|
|
166
|
-
# - Pre-built files from assets:precompile (already in asset pipeline)
|
|
167
|
-
# - Dynamically written files (written during runtime, also served through asset pipeline)
|
|
168
|
-
#
|
|
169
|
-
# The Set automatically deduplicates entries, so registering the same file multiple times
|
|
170
|
-
# (e.g., when the same component renders multiple times) is safe and efficient.
|
|
171
197
|
#
|
|
172
198
|
# @param file_path [String] Path to stylesheet (relative to app/assets/stylesheets)
|
|
173
199
|
# @param namespace [Symbol, String, nil] Optional namespace for separation (nil/blank uses default)
|
|
174
200
|
# @param options [Hash] Options for stylesheet_link_tag
|
|
175
201
|
# @return [void]
|
|
176
202
|
def self.register(file_path, namespace: nil, **options)
|
|
203
|
+
register_request_file(file_path, namespace: namespace, **options)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Register a stylesheet file path eagerly (process-wide manifest).
|
|
207
|
+
#
|
|
208
|
+
# Use at class load or boot time when the stylesheet should always be available in
|
|
209
|
+
# +render_head_stylesheets+ without waiting for a component render.
|
|
210
|
+
#
|
|
211
|
+
# @param file_path [String] Path to stylesheet (relative to app/assets/stylesheets)
|
|
212
|
+
# @param namespace [Symbol, String, nil] Optional namespace for separation (nil/blank uses default)
|
|
213
|
+
# @param options [Hash] Options for stylesheet_link_tag
|
|
214
|
+
# @return [void]
|
|
215
|
+
def self.register_eager(file_path, namespace: nil, **options)
|
|
177
216
|
ns = normalize_namespace(namespace)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
217
|
+
path = AssetPath.validate_logical_path!(file_path)
|
|
218
|
+
|
|
219
|
+
Instrumentation.instrument_registration(
|
|
220
|
+
namespace: ns,
|
|
221
|
+
file_path: path,
|
|
222
|
+
inline_size: nil,
|
|
223
|
+
cache_strategy: :none
|
|
224
|
+
) do
|
|
225
|
+
@manifest[ns] ||= {}
|
|
226
|
+
@manifest[ns][path] = {file_path: path, options: options}
|
|
227
|
+
end
|
|
183
228
|
end
|
|
184
229
|
|
|
230
|
+
# @api private
|
|
231
|
+
def self.register_request_file(file_path, namespace: nil, **options)
|
|
232
|
+
ns = normalize_namespace(namespace)
|
|
233
|
+
path = AssetPath.validate_logical_path!(file_path)
|
|
234
|
+
|
|
235
|
+
Instrumentation.instrument_registration(
|
|
236
|
+
namespace: ns,
|
|
237
|
+
file_path: path,
|
|
238
|
+
inline_size: nil,
|
|
239
|
+
cache_strategy: :none
|
|
240
|
+
) do
|
|
241
|
+
registry = request_file_stylesheets
|
|
242
|
+
registry[ns] ||= {}
|
|
243
|
+
registry[ns][path] = {file_path: path, options: options}
|
|
244
|
+
self.request_file_stylesheets = registry
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
private_class_method :register_request_file
|
|
248
|
+
|
|
185
249
|
# Register inline CSS for head rendering
|
|
186
250
|
#
|
|
187
251
|
# Inline CSS can be cached based on cache configuration. Supports:
|
|
@@ -201,6 +265,7 @@ module StyleCapsule
|
|
|
201
265
|
# @param component_class [Class, nil] Component class (for :file strategy)
|
|
202
266
|
# @param stylesheet_link_options [Hash, nil] Options for stylesheet_link_tag (for :file strategy)
|
|
203
267
|
# @return [void]
|
|
268
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- coordinates file, inline, and cache registration
|
|
204
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)
|
|
205
270
|
ns = normalize_namespace(namespace)
|
|
206
271
|
|
|
@@ -215,7 +280,7 @@ module StyleCapsule
|
|
|
215
280
|
|
|
216
281
|
if existing_path
|
|
217
282
|
# File exists (pre-built or previously written), register it as a file path
|
|
218
|
-
# The
|
|
283
|
+
# The manifest deduplicates by logical path if the same file is registered multiple times
|
|
219
284
|
# This file will be served through Rails asset pipeline (stylesheet_link_tag)
|
|
220
285
|
link_options = stylesheet_link_options || {}
|
|
221
286
|
register(existing_path, namespace: namespace, **link_options)
|
|
@@ -248,21 +313,28 @@ module StyleCapsule
|
|
|
248
313
|
# Use cached CSS if available, otherwise use provided CSS
|
|
249
314
|
final_css = cached_css || css_content
|
|
250
315
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
316
|
+
Instrumentation.instrument_registration(
|
|
317
|
+
namespace: ns,
|
|
318
|
+
file_path: nil,
|
|
319
|
+
inline_size: final_css.bytesize,
|
|
320
|
+
cache_strategy: cache_strategy
|
|
321
|
+
) do
|
|
322
|
+
registry = inline_stylesheets
|
|
323
|
+
registry[ns] ||= []
|
|
324
|
+
registry[ns] << {
|
|
325
|
+
type: :inline,
|
|
326
|
+
css_content: final_css,
|
|
327
|
+
capsule_id: capsule_id
|
|
328
|
+
}
|
|
329
|
+
self.inline_stylesheets = registry
|
|
330
|
+
end
|
|
260
331
|
|
|
261
332
|
# Cache the CSS if strategy is enabled and not already cached
|
|
262
333
|
if cache_strategy != :none && cache_key && !cached_css && cache_strategy != :file
|
|
263
334
|
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)
|
|
264
335
|
end
|
|
265
336
|
end
|
|
337
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
266
338
|
|
|
267
339
|
# Get cached inline CSS if available and not expired
|
|
268
340
|
#
|
|
@@ -291,7 +363,9 @@ module StyleCapsule
|
|
|
291
363
|
return nil if cache_ttl && cached_entry[:expires_at] && current_time > cached_entry[:expires_at]
|
|
292
364
|
when :proc
|
|
293
365
|
return nil unless cache_proc
|
|
294
|
-
#
|
|
366
|
+
# Re-validation: the proc is invoked on every read with the *current* request's CSS.
|
|
367
|
+
# Return [_, true, _] from the proc to keep using the cached entry; false invalidates it.
|
|
368
|
+
# (This is closer to a conditional cache than a blind key/value store.)
|
|
295
369
|
_key, should_use, _expires = cache_proc.call(css_content, capsule_id, namespace)
|
|
296
370
|
return nil unless should_use
|
|
297
371
|
end
|
|
@@ -401,9 +475,9 @@ module StyleCapsule
|
|
|
401
475
|
|
|
402
476
|
# Get all registered file paths from process-wide manifest (organized by namespace)
|
|
403
477
|
#
|
|
404
|
-
# @return [Hash<Symbol,
|
|
478
|
+
# @return [Hash<Symbol, Array<Hash>>] Hash of namespace => array of file registrations
|
|
405
479
|
def self.manifest_files
|
|
406
|
-
@manifest.
|
|
480
|
+
@manifest.transform_values { |h| h.values }
|
|
407
481
|
end
|
|
408
482
|
|
|
409
483
|
# Get all registered inline stylesheets for current request (organized by namespace)
|
|
@@ -413,38 +487,51 @@ module StyleCapsule
|
|
|
413
487
|
inline_stylesheets
|
|
414
488
|
end
|
|
415
489
|
|
|
490
|
+
# Get all request-scoped file stylesheets for the current request (organized by namespace)
|
|
491
|
+
#
|
|
492
|
+
# @return [Hash<Symbol, Hash<String, Hash>>] Hash of namespace => logical path => registration
|
|
493
|
+
def self.request_stylesheet_files
|
|
494
|
+
request_file_stylesheets
|
|
495
|
+
end
|
|
496
|
+
|
|
416
497
|
# Get all stylesheets (files + inline) for a specific namespace
|
|
417
498
|
#
|
|
418
499
|
# @param namespace [Symbol, String, nil] Namespace identifier (nil/blank uses default)
|
|
419
500
|
# @return [Array<Hash>] Array of stylesheet registrations for the namespace
|
|
420
501
|
def self.stylesheets_for(namespace: nil)
|
|
421
502
|
ns = normalize_namespace(namespace)
|
|
422
|
-
result =
|
|
423
|
-
|
|
424
|
-
# Add process-wide file paths
|
|
425
|
-
if @manifest[ns]
|
|
426
|
-
result.concat(@manifest[ns].to_a)
|
|
427
|
-
end
|
|
503
|
+
result = merged_file_registrations_for_namespace(ns)
|
|
428
504
|
|
|
429
|
-
# Add request-scoped inline CSS
|
|
430
505
|
inline = request_inline_stylesheets[ns] || []
|
|
431
506
|
result.concat(inline)
|
|
432
507
|
|
|
433
508
|
result
|
|
434
509
|
end
|
|
435
510
|
|
|
436
|
-
#
|
|
511
|
+
# Whether request-scoped stylesheets remain to inject into +<head>+
|
|
512
|
+
#
|
|
513
|
+
# @return [Boolean]
|
|
514
|
+
def self.pending_head_stylesheets?
|
|
515
|
+
!pending_request_stylesheets.empty?
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Clear request-scoped inline CSS and render-time file registrations (does not clear process-wide manifest)
|
|
437
519
|
#
|
|
438
520
|
# @param namespace [Symbol, String, nil] Optional namespace to clear (nil clears all)
|
|
439
521
|
# @return [void]
|
|
440
522
|
def self.clear(namespace: nil)
|
|
441
523
|
if namespace.nil?
|
|
442
524
|
self.inline_stylesheets = {}
|
|
525
|
+
self.request_file_stylesheets = {}
|
|
443
526
|
else
|
|
444
527
|
ns = normalize_namespace(namespace)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
self.inline_stylesheets =
|
|
528
|
+
inline_registry = inline_stylesheets
|
|
529
|
+
inline_registry.delete(ns)
|
|
530
|
+
self.inline_stylesheets = inline_registry
|
|
531
|
+
|
|
532
|
+
file_registry = request_file_stylesheets
|
|
533
|
+
file_registry.delete(ns)
|
|
534
|
+
self.request_file_stylesheets = file_registry
|
|
448
535
|
end
|
|
449
536
|
end
|
|
450
537
|
|
|
@@ -464,8 +551,12 @@ module StyleCapsule
|
|
|
464
551
|
# Render registered stylesheets as HTML
|
|
465
552
|
#
|
|
466
553
|
# This should be called in the layout's <head> section.
|
|
467
|
-
# Combines
|
|
468
|
-
#
|
|
554
|
+
# Combines eager manifest files with request-scoped file paths and inline CSS
|
|
555
|
+
# registered before this call. Stylesheets registered later in the response are
|
|
556
|
+
# injected into +<head>+ by +HeadInjectionMiddleware+ when enabled.
|
|
557
|
+
#
|
|
558
|
+
# Automatically clears request-scoped inline CSS and file paths after rendering
|
|
559
|
+
# for the selected namespace(s). The eager manifest persists.
|
|
469
560
|
#
|
|
470
561
|
# @param view_context [ActionView::Base, nil] The view context (for helpers like content_tag, stylesheet_link_tag)
|
|
471
562
|
# In ERB: pass `self` (the view context)
|
|
@@ -473,22 +564,16 @@ module StyleCapsule
|
|
|
473
564
|
# If nil, falls back to basic HTML generation
|
|
474
565
|
# @param namespace [Symbol, String, nil] Optional namespace to render (nil/blank renders all namespaces)
|
|
475
566
|
# @return [String] HTML-safe string with stylesheet tags
|
|
567
|
+
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity -- renders mixed inline and file registrations
|
|
476
568
|
def self.render_head_stylesheets(view_context = nil, namespace: nil)
|
|
477
569
|
if namespace.nil? || namespace.to_s.strip.empty?
|
|
478
|
-
|
|
479
|
-
all_stylesheets = []
|
|
570
|
+
all_stylesheets = merged_file_registrations_all_namespaces
|
|
480
571
|
|
|
481
|
-
# Collect from process-wide manifest (all namespaces)
|
|
482
|
-
@manifest.each do |ns, files|
|
|
483
|
-
all_stylesheets.concat(files.to_a)
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
# Collect from request-scoped inline CSS (all namespaces)
|
|
487
572
|
request_inline_stylesheets.each do |_ns, inline|
|
|
488
573
|
all_stylesheets.concat(inline)
|
|
489
574
|
end
|
|
490
575
|
|
|
491
|
-
clear # Clear request-scoped inline CSS only
|
|
576
|
+
clear # Clear request-scoped inline CSS and file paths only
|
|
492
577
|
return safe_string("") if all_stylesheets.empty?
|
|
493
578
|
|
|
494
579
|
all_stylesheets.map do |stylesheet|
|
|
@@ -503,7 +588,7 @@ module StyleCapsule
|
|
|
503
588
|
# Render specific namespace
|
|
504
589
|
ns = normalize_namespace(namespace)
|
|
505
590
|
stylesheets = stylesheets_for(namespace: ns).dup
|
|
506
|
-
clear(namespace: ns) # Clear request-scoped inline CSS only
|
|
591
|
+
clear(namespace: ns) # Clear request-scoped inline CSS and file paths only
|
|
507
592
|
|
|
508
593
|
return safe_string("") if stylesheets.empty?
|
|
509
594
|
|
|
@@ -517,28 +602,123 @@ module StyleCapsule
|
|
|
517
602
|
|
|
518
603
|
end
|
|
519
604
|
end
|
|
605
|
+
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
520
606
|
|
|
521
607
|
# Check if there are any registered stylesheets
|
|
522
608
|
#
|
|
523
609
|
# @param namespace [Symbol, String, nil] Optional namespace to check (nil checks all)
|
|
524
610
|
# @return [Boolean]
|
|
611
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- checks manifest and request-scoped registries
|
|
525
612
|
def self.any?(namespace: nil)
|
|
526
613
|
if namespace.nil?
|
|
527
614
|
# Check process-wide manifest
|
|
528
615
|
has_files = !@manifest.empty? && @manifest.values.any? { |files| !files.empty? }
|
|
616
|
+
# Check request-scoped file paths
|
|
617
|
+
request_files = request_file_stylesheets
|
|
618
|
+
has_request_files = !request_files.empty? && request_files.values.any? { |files| !files.empty? }
|
|
529
619
|
# Check request-scoped inline CSS
|
|
530
620
|
inline = request_inline_stylesheets
|
|
531
621
|
has_inline = !inline.empty? && inline.values.any? { |stylesheets| !stylesheets.empty? }
|
|
532
|
-
has_files || has_inline
|
|
622
|
+
has_files || has_request_files || has_inline
|
|
533
623
|
else
|
|
534
624
|
ns = normalize_namespace(namespace)
|
|
535
625
|
# Check process-wide manifest
|
|
536
626
|
has_files = @manifest[ns] && !@manifest[ns].empty?
|
|
627
|
+
# Check request-scoped file paths
|
|
628
|
+
has_request_files = request_file_stylesheets[ns] && !request_file_stylesheets[ns].empty?
|
|
537
629
|
# Check request-scoped inline CSS
|
|
538
630
|
has_inline = request_inline_stylesheets[ns] && !request_inline_stylesheets[ns].empty?
|
|
539
|
-
!!(has_files || has_inline)
|
|
631
|
+
!!(has_files || has_request_files || has_inline)
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
635
|
+
|
|
636
|
+
# Inject pending request-scoped stylesheets into an HTML document before +</head>+.
|
|
637
|
+
#
|
|
638
|
+
# Used by +HeadInjectionMiddleware+ after the body has rendered and components have
|
|
639
|
+
# called +register_stylesheet+ or +register_inline+.
|
|
640
|
+
#
|
|
641
|
+
# @param html [String] Full HTML response body
|
|
642
|
+
# @param view_context [ActionView::Base, nil] View context for +stylesheet_link_tag+
|
|
643
|
+
# @return [String] HTML with pending stylesheet tags injected, or the original HTML
|
|
644
|
+
def self.inject_pending_head_stylesheets(html, view_context = nil)
|
|
645
|
+
pending_stylesheets = pending_request_stylesheets
|
|
646
|
+
return html if pending_stylesheets.empty?
|
|
647
|
+
|
|
648
|
+
closing_head_index = html.match(%r{</head>}i)&.begin(0)
|
|
649
|
+
return html unless closing_head_index
|
|
650
|
+
|
|
651
|
+
tags = render_stylesheet_tags(pending_stylesheets, view_context)
|
|
652
|
+
return html if tags.empty?
|
|
653
|
+
|
|
654
|
+
injected = html.dup
|
|
655
|
+
injected.insert(closing_head_index, "#{tags}\n")
|
|
656
|
+
clear
|
|
657
|
+
injected
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# @api private
|
|
661
|
+
def self.merged_file_registrations_for_namespace(namespace)
|
|
662
|
+
merge_file_registrations(@manifest[namespace], request_file_stylesheets[namespace])
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# @api private
|
|
666
|
+
def self.merged_file_registrations_all_namespaces
|
|
667
|
+
merged = {}
|
|
668
|
+
|
|
669
|
+
@manifest.each do |ns, files|
|
|
670
|
+
files.each_value do |entry|
|
|
671
|
+
merged[[ns, entry[:file_path]]] = entry
|
|
672
|
+
end
|
|
540
673
|
end
|
|
674
|
+
|
|
675
|
+
request_file_stylesheets.each do |ns, files|
|
|
676
|
+
files.each_value do |entry|
|
|
677
|
+
merged[[ns, entry[:file_path]]] = entry
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
merged.values
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# @api private
|
|
685
|
+
def self.merge_file_registrations(eager_files, request_files)
|
|
686
|
+
merged = {}
|
|
687
|
+
eager_files&.each_value { |entry| merged[entry[:file_path]] = entry }
|
|
688
|
+
request_files&.each_value { |entry| merged[entry[:file_path]] = entry }
|
|
689
|
+
merged.values
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# @api private
|
|
693
|
+
def self.pending_request_stylesheets
|
|
694
|
+
stylesheets = []
|
|
695
|
+
|
|
696
|
+
request_file_stylesheets.each_value do |files|
|
|
697
|
+
stylesheets.concat(files.values)
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
request_inline_stylesheets.each_value do |inline_styles|
|
|
701
|
+
stylesheets.concat(inline_styles)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
stylesheets
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# @api private
|
|
708
|
+
def self.render_stylesheet_tags(stylesheets, view_context)
|
|
709
|
+
return safe_string("") if stylesheets.empty?
|
|
710
|
+
|
|
711
|
+
stylesheets.map do |stylesheet|
|
|
712
|
+
if stylesheet[:type] == :inline
|
|
713
|
+
render_inline_stylesheet(stylesheet, view_context)
|
|
714
|
+
else
|
|
715
|
+
render_file_stylesheet(stylesheet, view_context)
|
|
716
|
+
end
|
|
717
|
+
end.join("\n").then { |output| safe_string(output) }.to_s
|
|
541
718
|
end
|
|
719
|
+
private_class_method :pending_request_stylesheets, :render_stylesheet_tags,
|
|
720
|
+
:merged_file_registrations_for_namespace, :merged_file_registrations_all_namespaces,
|
|
721
|
+
:merge_file_registrations
|
|
542
722
|
|
|
543
723
|
# Render a file-based stylesheet
|
|
544
724
|
def self.render_file_stylesheet(stylesheet, view_context)
|
|
@@ -548,7 +728,7 @@ module StyleCapsule
|
|
|
548
728
|
if view_context&.respond_to?(:stylesheet_link_tag)
|
|
549
729
|
view_context.stylesheet_link_tag(file_path, **options)
|
|
550
730
|
else
|
|
551
|
-
# Fallback if no view context
|
|
731
|
+
# Fallback if no view context (logical path validated in .register)
|
|
552
732
|
href = "/assets/#{file_path}.css"
|
|
553
733
|
tag_options = options.map { |k, v| %(#{k}="#{v}") }.join(" ")
|
|
554
734
|
safe_string(%(<link rel="stylesheet" href="#{href}"#{" #{tag_options}" unless tag_options.empty?}>))
|