style_capsule 1.0.2 → 1.2.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.
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ require "cgi"
5
+
6
+ module StyleCapsule
7
+ # Standalone helper module for use without Rails
8
+ #
9
+ # This module provides basic HTML generation and CSS scoping functionality
10
+ # without requiring Rails ActionView helpers. It can be included in any
11
+ # framework's view context or used directly.
12
+ #
13
+ # @example Usage in Sinatra
14
+ # class MyApp < Sinatra::Base
15
+ # helpers StyleCapsule::StandaloneHelper
16
+ # end
17
+ #
18
+ # @example Usage in plain Ruby
19
+ # class MyView
20
+ # include StyleCapsule::StandaloneHelper
21
+ # end
22
+ #
23
+ # @example Usage in ERB (non-Rails)
24
+ # # In your ERB template context
25
+ # include StyleCapsule::StandaloneHelper
26
+ # style_capsule do
27
+ # "<style>.section { color: red; }</style><div class='section'>Content</div>"
28
+ # end
29
+ module StandaloneHelper
30
+ # Maximum HTML content size (10MB) to prevent DoS attacks
31
+ MAX_HTML_SIZE = 10_000_000
32
+
33
+ # Generate capsule ID based on caller location for uniqueness
34
+ def generate_capsule_id(css_content)
35
+ # Use caller location + CSS content for uniqueness
36
+ caller_info = caller_locations(1, 1).first
37
+ capsule_key = "#{caller_info.path}:#{caller_info.lineno}:#{css_content}"
38
+ "a#{Digest::SHA1.hexdigest(capsule_key)}"[0, 8]
39
+ end
40
+
41
+ # Scope CSS content and return scoped CSS
42
+ def scope_css(css_content, capsule_id)
43
+ # Use thread-local cache to avoid reprocessing
44
+ cache_key = "style_capsule_#{capsule_id}"
45
+
46
+ if Thread.current[cache_key]
47
+ return Thread.current[cache_key]
48
+ end
49
+
50
+ scoped_css = CssProcessor.scope_selectors(css_content, capsule_id)
51
+ Thread.current[cache_key] = scoped_css
52
+ scoped_css
53
+ end
54
+
55
+ # Generate HTML tag without Rails helpers
56
+ #
57
+ # @param tag [String, Symbol] HTML tag name
58
+ # @param content [String, nil] Tag content (or use block)
59
+ # @param options [Hash] HTML attributes
60
+ # @param block [Proc] Block for tag content
61
+ # @return [String] HTML string
62
+ def content_tag(tag, content = nil, **options, &block)
63
+ tag_name = tag.to_s
64
+ content = capture(&block) if block_given? && content.nil?
65
+ content ||= ""
66
+
67
+ attrs = options.map do |k, v|
68
+ if v.is_a?(Hash)
69
+ # Handle nested attributes like data: { capsule: "abc" }
70
+ v.map { |nk, nv| %(#{k}-#{nk}="#{escape_html_attr(nv)}") }.join(" ")
71
+ else
72
+ %(#{k}="#{escape_html_attr(v)}")
73
+ end
74
+ end.join(" ")
75
+
76
+ attrs = " #{attrs}" unless attrs.empty?
77
+ "<#{tag_name}#{attrs}>#{content}</#{tag_name}>"
78
+ end
79
+
80
+ # Capture block content (simplified version without Rails)
81
+ #
82
+ # @param block [Proc] Block to capture
83
+ # @return [String] Captured content
84
+ def capture(&block)
85
+ return "" unless block_given?
86
+ block.call.to_s
87
+ end
88
+
89
+ # Mark string as HTML-safe (for compatibility)
90
+ #
91
+ # @param string [String] String to mark as safe
92
+ # @return [String] HTML-safe string
93
+ def html_safe(string)
94
+ # In non-Rails context, just return the string
95
+ # If ActiveSupport is available, use its html_safe
96
+ if string.respond_to?(:html_safe)
97
+ string.html_safe
98
+ else
99
+ string
100
+ end
101
+ end
102
+
103
+ # Raw string (no HTML escaping)
104
+ #
105
+ # @param string [String] String to return as-is
106
+ # @return [String] Raw string
107
+ def raw(string)
108
+ html_safe(string.to_s)
109
+ end
110
+
111
+ # ERB helper: automatically wraps content in scoped div and processes CSS
112
+ #
113
+ # @param css_content [String, nil] CSS content (or extract from block)
114
+ # @param capsule_id [String, nil] Optional capsule ID
115
+ # @param content_block [Proc] Block containing HTML content
116
+ # @return [String] HTML with scoped CSS and wrapped content
117
+ def style_capsule(css_content = nil, capsule_id: nil, &content_block)
118
+ html_content = nil
119
+
120
+ # If CSS content is provided as argument, use it
121
+ # Otherwise, extract from content block
122
+ if css_content.nil? && block_given?
123
+ full_content = capture(&content_block)
124
+
125
+ # Validate HTML content size to prevent DoS attacks
126
+ if full_content.bytesize > MAX_HTML_SIZE
127
+ raise ArgumentError, "HTML content exceeds maximum size of #{MAX_HTML_SIZE} bytes (got #{full_content.bytesize} bytes)"
128
+ end
129
+
130
+ # Extract <style> tags from content
131
+ style_match = full_content.match(/<style[^>]*>(.*?)<\/style>/m)
132
+ if style_match
133
+ css_content = style_match[1]
134
+ html_content = full_content.sub(/<style[^>]*>.*?<\/style>/m, "").strip
135
+ else
136
+ css_content = nil
137
+ html_content = full_content
138
+ end
139
+ elsif css_content && block_given?
140
+ html_content = capture(&content_block)
141
+ elsif css_content && !block_given?
142
+ # CSS provided but no content block - just return scoped CSS
143
+ capsule_id ||= generate_capsule_id(css_content)
144
+ scoped_css = scope_css(css_content, capsule_id)
145
+ return content_tag(:style, raw(scoped_css), type: "text/css")
146
+ else
147
+ return ""
148
+ end
149
+
150
+ # If no CSS, just return content
151
+ return html_safe(html_content) if css_content.nil? || css_content.to_s.strip.empty?
152
+
153
+ # Use provided capsule_id or generate one
154
+ capsule_id ||= generate_capsule_id(css_content)
155
+ scoped_css = scope_css(css_content, capsule_id)
156
+
157
+ # Render style tag and wrapped content
158
+ style_tag = content_tag(:style, raw(scoped_css), type: "text/css")
159
+ wrapped_content = content_tag(:div, raw(html_content), data: {capsule: capsule_id})
160
+
161
+ html_safe(style_tag + wrapped_content)
162
+ end
163
+
164
+ # Register a stylesheet file for head rendering
165
+ #
166
+ # @param file_path [String] Path to stylesheet
167
+ # @param namespace [Symbol, String, nil] Optional namespace
168
+ # @param options [Hash] Options for stylesheet link tag
169
+ # @return [void]
170
+ def register_stylesheet(file_path, namespace: nil, **options)
171
+ StyleCapsule::StylesheetRegistry.register(file_path, namespace: namespace, **options)
172
+ end
173
+
174
+ # Render StyleCapsule registered stylesheets
175
+ #
176
+ # @param namespace [Symbol, String, nil] Optional namespace to render
177
+ # @return [String] HTML-safe string with stylesheet tags
178
+ def stylesheet_registry_tags(namespace: nil)
179
+ StyleCapsule::StylesheetRegistry.render_head_stylesheets(self, namespace: namespace)
180
+ end
181
+
182
+ # @deprecated Use {#stylesheet_registry_tags} instead.
183
+ # This method name will be removed in a future version.
184
+ alias_method :stylesheet_registrymap_tags, :stylesheet_registry_tags
185
+
186
+ private
187
+
188
+ # Escape HTML attribute value
189
+ #
190
+ # @param value [String] Value to escape
191
+ # @return [String] Escaped value
192
+ def escape_html_attr(value)
193
+ CGI.escapeHTML(value.to_s)
194
+ end
195
+ end
196
+ end
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/current_attributes"
4
-
5
3
  module StyleCapsule
4
+ # Helper to determine the parent class for StylesheetRegistry
5
+ # ActiveSupport::CurrentAttributes is optional - if ActiveSupport is loaded,
6
+ # it will be available. Otherwise, we fall back to Object.
7
+ #
8
+ # This is evaluated at class definition time, so it can't be stubbed.
9
+ # For testing fallback paths, use the instance methods that check availability.
10
+ def self.stylesheet_registry_parent_class
11
+ defined?(ActiveSupport::CurrentAttributes) ? ActiveSupport::CurrentAttributes : Object
12
+ end
13
+
6
14
  # Hybrid registry for stylesheet files that need to be injected into <head>
7
15
  #
8
16
  # Uses a process-wide manifest for static file paths (like Propshaft) and request-scoped
@@ -52,7 +60,7 @@ module StyleCapsule
52
60
  # body(&block)
53
61
  # end
54
62
  # end
55
- class StylesheetRegistry < ActiveSupport::CurrentAttributes
63
+ class StylesheetRegistry < StyleCapsule.stylesheet_registry_parent_class
56
64
  # Default namespace for backward compatibility
57
65
  DEFAULT_NAMESPACE = :default
58
66
 
@@ -68,10 +76,74 @@ module StyleCapsule
68
76
  @last_cleanup_time = nil # rubocop:disable Style/ClassVars
69
77
 
70
78
  # Request-scoped storage for inline CSS only
71
- attribute :inline_stylesheets
79
+ # Only define attribute if we're inheriting from CurrentAttributes
80
+ if defined?(ActiveSupport::CurrentAttributes) && self < ActiveSupport::CurrentAttributes
81
+ attribute :inline_stylesheets
82
+ end
72
83
 
73
84
  class << self
74
85
  attr_reader :manifest, :inline_cache
86
+
87
+ # Get current time (ActiveSupport::Time.current or Time.now fallback)
88
+ def current_time
89
+ if defined?(Time) && Time.respond_to?(:current)
90
+ Time.current
91
+ else
92
+ # rubocop:disable Rails/TimeZone
93
+ # Time.now is intentional fallback for non-Rails usage when Time.current is unavailable
94
+ Time.now
95
+ # rubocop:enable Rails/TimeZone
96
+ end
97
+ end
98
+
99
+ # Check if we're using ActiveSupport::CurrentAttributes
100
+ # This method can be stubbed in tests to test fallback paths
101
+ def using_current_attributes?
102
+ defined?(ActiveSupport::CurrentAttributes) && self < ActiveSupport::CurrentAttributes
103
+ end
104
+
105
+ # Get inline stylesheets (thread-local fallback if not using CurrentAttributes)
106
+ def inline_stylesheets
107
+ if using_current_attributes?
108
+ # When using CurrentAttributes, access the instance attribute
109
+ # CurrentAttributes automatically provides access to instance attributes
110
+ inst = instance
111
+ inst&.inline_stylesheets || {}
112
+ else
113
+ Thread.current[:style_capsule_inline_stylesheets] ||= {}
114
+ end
115
+ end
116
+
117
+ # Set inline stylesheets (thread-local fallback if not using CurrentAttributes)
118
+ def inline_stylesheets=(value)
119
+ if using_current_attributes?
120
+ # When using CurrentAttributes, set via the instance
121
+ inst = instance
122
+ inst.inline_stylesheets = value if inst
123
+ else
124
+ Thread.current[:style_capsule_inline_stylesheets] = value
125
+ end
126
+ end
127
+
128
+ # Get instance (for CurrentAttributes compatibility)
129
+ def instance
130
+ if using_current_attributes?
131
+ # Call the CurrentAttributes instance method from parent class
132
+ super
133
+ else
134
+ # Return a simple object that responds to inline_stylesheets
135
+ # This is mainly for compatibility with code that might call instance.inline_stylesheets
136
+ registry_class = self
137
+ @_standalone_instance ||= begin
138
+ obj = Object.new
139
+ obj.define_singleton_method(:inline_stylesheets) { registry_class.inline_stylesheets }
140
+ obj.define_singleton_method(:inline_stylesheets=) { |v| registry_class.inline_stylesheets = v }
141
+ obj
142
+ end
143
+ end
144
+ end
145
+
146
+ private
75
147
  end
76
148
 
77
149
  # Normalize namespace (nil/blank becomes DEFAULT_NAMESPACE)
@@ -177,14 +249,14 @@ module StyleCapsule
177
249
  final_css = cached_css || css_content
178
250
 
179
251
  # Store in request-scoped registry
180
- registry = instance.inline_stylesheets || {}
252
+ registry = inline_stylesheets
181
253
  registry[ns] ||= []
182
254
  registry[ns] << {
183
255
  type: :inline,
184
256
  css_content: final_css,
185
257
  capsule_id: capsule_id
186
258
  }
187
- instance.inline_stylesheets = registry
259
+ self.inline_stylesheets = registry
188
260
 
189
261
  # Cache the CSS if strategy is enabled and not already cached
190
262
  if cache_strategy != :none && cache_key && !cached_css && cache_strategy != :file
@@ -216,7 +288,7 @@ module StyleCapsule
216
288
  # Check expiration based on strategy
217
289
  case cache_strategy
218
290
  when :time
219
- return nil if cache_ttl && cached_entry[:expires_at] && Time.current > cached_entry[:expires_at]
291
+ return nil if cache_ttl && cached_entry[:expires_at] && current_time > cached_entry[:expires_at]
220
292
  when :proc
221
293
  return nil unless cache_proc
222
294
  # Proc should validate cache entry
@@ -242,7 +314,13 @@ module StyleCapsule
242
314
 
243
315
  case cache_strategy
244
316
  when :time
245
- expires_at = cache_ttl ? Time.current + cache_ttl : nil
317
+ # Handle ActiveSupport::Duration (e.g., 1.hour) or integer seconds
318
+ ttl_seconds = if cache_ttl.respond_to?(:to_i)
319
+ cache_ttl.to_i
320
+ else
321
+ cache_ttl
322
+ end
323
+ expires_at = ttl_seconds ? current_time + ttl_seconds : nil
246
324
  when :proc
247
325
  if cache_proc
248
326
  _key, _should_cache, proc_expires = cache_proc.call(css_content, capsule_id, namespace)
@@ -252,7 +330,7 @@ module StyleCapsule
252
330
 
253
331
  @inline_cache[cache_key] = {
254
332
  css_content: css_content,
255
- cached_at: Time.current,
333
+ cached_at: current_time,
256
334
  expires_at: expires_at
257
335
  }
258
336
  end
@@ -287,18 +365,18 @@ module StyleCapsule
287
365
  def self.cleanup_expired_cache
288
366
  return 0 if @inline_cache.empty?
289
367
 
290
- current_time = Time.current
368
+ now = current_time
291
369
  expired_keys = []
292
370
 
293
371
  @inline_cache.each do |cache_key, entry|
294
372
  # Remove entries that have an expires_at time and it's in the past
295
- if entry[:expires_at] && current_time > entry[:expires_at]
373
+ if entry[:expires_at] && now > entry[:expires_at]
296
374
  expired_keys << cache_key
297
375
  end
298
376
  end
299
377
 
300
378
  expired_keys.each { |key| @inline_cache.delete(key) }
301
- @last_cleanup_time = current_time
379
+ @last_cleanup_time = now
302
380
 
303
381
  expired_keys.size
304
382
  end
@@ -314,7 +392,7 @@ module StyleCapsule
314
392
  # Cleanup every 5 minutes (300 seconds) to balance memory usage and performance
315
393
  cleanup_interval = 300
316
394
 
317
- if @last_cleanup_time.nil? || (Time.current - @last_cleanup_time) > cleanup_interval
395
+ if @last_cleanup_time.nil? || (current_time - @last_cleanup_time) > cleanup_interval
318
396
  cleanup_expired_cache
319
397
  end
320
398
  end
@@ -332,7 +410,7 @@ module StyleCapsule
332
410
  #
333
411
  # @return [Hash<Symbol, Array<Hash>>] Hash of namespace => array of inline stylesheet registrations
334
412
  def self.request_inline_stylesheets
335
- instance.inline_stylesheets || {}
413
+ inline_stylesheets
336
414
  end
337
415
 
338
416
  # Get all stylesheets (files + inline) for a specific namespace
@@ -361,12 +439,12 @@ module StyleCapsule
361
439
  # @return [void]
362
440
  def self.clear(namespace: nil)
363
441
  if namespace.nil?
364
- instance.inline_stylesheets = {}
442
+ self.inline_stylesheets = {}
365
443
  else
366
444
  ns = normalize_namespace(namespace)
367
- registry = instance.inline_stylesheets || {}
445
+ registry = inline_stylesheets
368
446
  registry.delete(ns)
369
- instance.inline_stylesheets = registry
447
+ self.inline_stylesheets = registry
370
448
  end
371
449
  end
372
450
 
@@ -411,7 +489,7 @@ module StyleCapsule
411
489
  end
412
490
 
413
491
  clear # Clear request-scoped inline CSS only
414
- return "".html_safe if all_stylesheets.empty?
492
+ return safe_string("") if all_stylesheets.empty?
415
493
 
416
494
  all_stylesheets.map do |stylesheet|
417
495
  if stylesheet[:type] == :inline
@@ -419,7 +497,7 @@ module StyleCapsule
419
497
  else
420
498
  render_file_stylesheet(stylesheet, view_context)
421
499
  end
422
- end.join("\n").html_safe
500
+ end.join("\n").then { |s| safe_string(s) }
423
501
 
424
502
  else
425
503
  # Render specific namespace
@@ -427,7 +505,7 @@ module StyleCapsule
427
505
  stylesheets = stylesheets_for(namespace: ns).dup
428
506
  clear(namespace: ns) # Clear request-scoped inline CSS only
429
507
 
430
- return "".html_safe if stylesheets.empty?
508
+ return safe_string("") if stylesheets.empty?
431
509
 
432
510
  stylesheets.map do |stylesheet|
433
511
  if stylesheet[:type] == :inline
@@ -435,7 +513,7 @@ module StyleCapsule
435
513
  else
436
514
  render_file_stylesheet(stylesheet, view_context)
437
515
  end
438
- end.join("\n").html_safe
516
+ end.join("\n").then { |s| safe_string(s) }
439
517
 
440
518
  end
441
519
  end
@@ -473,7 +551,7 @@ module StyleCapsule
473
551
  # Fallback if no view context
474
552
  href = "/assets/#{file_path}.css"
475
553
  tag_options = options.map { |k, v| %(#{k}="#{v}") }.join(" ")
476
- %(<link rel="stylesheet" href="#{href}"#{" #{tag_options}" unless tag_options.empty?}>).html_safe
554
+ safe_string(%(<link rel="stylesheet" href="#{href}"#{" #{tag_options}" unless tag_options.empty?}>))
477
555
  end
478
556
  end
479
557
 
@@ -486,9 +564,21 @@ module StyleCapsule
486
564
  # Construct HTML manually to avoid any HTML escaping issues
487
565
  # CSS content should not be HTML-escaped as it's inside a <style> tag
488
566
  # Using string interpolation with html_safe ensures CSS is not escaped
489
- %(<style type="text/css">#{css_content}</style>).html_safe
567
+ safe_string(%(<style type="text/css">#{css_content}</style>))
568
+ end
569
+
570
+ # Make string HTML-safe (compatible with Rails and non-Rails)
571
+ #
572
+ # @param string [String] String to mark as safe
573
+ # @return [String] HTML-safe string
574
+ def self.safe_string(string)
575
+ if string.respond_to?(:html_safe)
576
+ string.html_safe
577
+ else
578
+ string
579
+ end
490
580
  end
491
581
 
492
- private_class_method :render_file_stylesheet, :render_inline_stylesheet
582
+ private_class_method :render_file_stylesheet, :render_inline_stylesheet, :safe_string
493
583
  end
494
584
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StyleCapsule
4
- VERSION = "1.0.2"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest/sha1"
4
- require "active_support/core_ext/string"
4
+ # ActiveSupport string extensions are conditionally required in lib/style_capsule.rb
5
5
 
6
6
  module StyleCapsule
7
7
  # ViewComponent component concern for encapsulated CSS
@@ -89,6 +89,9 @@ module StyleCapsule
89
89
 
90
90
  # Use prepend to wrap call method
91
91
  base.prepend(CallWrapper)
92
+
93
+ # Register class for Rails-friendly tracking
94
+ ClassRegistry.register(base)
92
95
  end
93
96
 
94
97
  module ClassMethods
@@ -36,18 +36,22 @@ module StyleCapsule
36
36
  StyleCapsule::StylesheetRegistry.register(file_path, namespace: namespace, **options)
37
37
  end
38
38
 
39
- # Render StyleCapsule registered stylesheets (similar to javascript_importmap_tags)
39
+ # Render StyleCapsule registered stylesheets
40
40
  #
41
41
  # Usage in ViewComponent layouts:
42
42
  # def call
43
- # helpers.stylesheet_registrymap_tags
44
- # helpers.stylesheet_registrymap_tags(namespace: :admin)
43
+ # helpers.stylesheet_registry_tags
44
+ # helpers.stylesheet_registry_tags(namespace: :admin)
45
45
  # end
46
46
  #
47
47
  # @param namespace [Symbol, String, nil] Optional namespace to render (nil/blank renders all)
48
48
  # @return [String] HTML-safe string with stylesheet tags
49
- def stylesheet_registrymap_tags(namespace: nil)
49
+ def stylesheet_registry_tags(namespace: nil)
50
50
  StyleCapsule::StylesheetRegistry.render_head_stylesheets(helpers, namespace: namespace)
51
51
  end
52
+
53
+ # @deprecated Use {#stylesheet_registry_tags} instead.
54
+ # This method name will be removed in a future version.
55
+ alias_method :stylesheet_registrymap_tags, :stylesheet_registry_tags
52
56
  end
53
57
  end
data/lib/style_capsule.rb CHANGED
@@ -1,7 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest/sha1"
4
- require "active_support/core_ext/string"
4
+ # Conditionally require ActiveSupport string extensions if available
5
+ # For non-Rails usage, these are optional
6
+ # Check first to avoid exception handling overhead in common case (Rails apps)
7
+ unless defined?(ActiveSupport) || String.method_defined?(:html_safe)
8
+ begin
9
+ require "active_support/core_ext/string"
10
+ rescue LoadError
11
+ # ActiveSupport not available - core functionality still works
12
+ end
13
+ end
5
14
 
6
15
  # StyleCapsule provides attribute-based CSS scoping for component encapsulation
7
16
  # in Phlex components, ViewComponent components, and ERB templates.
@@ -55,8 +64,8 @@ require "active_support/core_ext/string"
55
64
  # <div class="section">Content</div>
56
65
  # <% end %>
57
66
  #
58
- # <%= stylesheet_registrymap_tags %>
59
- # <%= stylesheet_registrymap_tags(namespace: :admin) %>
67
+ # <%= stylesheet_registry_tags %>
68
+ # <%= stylesheet_registry_tags(namespace: :admin) %>
60
69
  #
61
70
  # @example Namespace Support
62
71
  # # Register stylesheets with namespaces
@@ -64,10 +73,10 @@ require "active_support/core_ext/string"
64
73
  # StyleCapsule::StylesheetRegistry.register('stylesheets/user/profile', namespace: :user)
65
74
  #
66
75
  # # Render all namespaces (default)
67
- # <%= stylesheet_registrymap_tags %>
76
+ # <%= stylesheet_registry_tags %>
68
77
  #
69
78
  # # Render specific namespace
70
- # <%= stylesheet_registrymap_tags(namespace: :admin) %>
79
+ # <%= stylesheet_registry_tags(namespace: :admin) %>
71
80
  #
72
81
  # @example File-Based Caching (HTTP Caching)
73
82
  # class MyComponent < ApplicationComponent
@@ -83,11 +92,14 @@ module StyleCapsule
83
92
  require_relative "style_capsule/css_processor"
84
93
  require_relative "style_capsule/css_file_writer"
85
94
  require_relative "style_capsule/stylesheet_registry"
95
+ require_relative "style_capsule/class_registry"
86
96
  require_relative "style_capsule/component_styles_support"
87
97
  require_relative "style_capsule/component"
98
+ require_relative "style_capsule/standalone_helper"
88
99
  require_relative "style_capsule/helper"
89
100
  require_relative "style_capsule/phlex_helper"
90
101
  require_relative "style_capsule/view_component"
91
102
  require_relative "style_capsule/view_component_helper"
103
+ require_relative "style_capsule/component_builder"
92
104
  require_relative "style_capsule/railtie" if defined?(Rails) && defined?(Rails::Railtie)
93
105
  end
@@ -3,76 +3,9 @@
3
3
  namespace :style_capsule do
4
4
  desc "Build StyleCapsule CSS files from components (similar to Tailwind CSS build)"
5
5
  task build: :environment do
6
- require "style_capsule/css_file_writer"
7
-
8
- # Ensure output directory exists
9
- StyleCapsule::CssFileWriter.ensure_output_directory
10
-
11
- # Collect all component classes that use StyleCapsule
12
- component_classes = []
13
-
14
- # Find Phlex components
15
- if defined?(Phlex::HTML)
16
- ObjectSpace.each_object(Class) do |klass|
17
- if klass < Phlex::HTML && klass.included_modules.include?(StyleCapsule::Component)
18
- component_classes << klass
19
- end
20
- end
21
- end
22
-
23
- # Find ViewComponent components
24
- # Wrap in begin/rescue to handle ViewComponent loading errors (e.g., version compatibility issues)
25
- begin
26
- if defined?(ViewComponent::Base)
27
- ObjectSpace.each_object(Class) do |klass|
28
- if klass < ViewComponent::Base && klass.included_modules.include?(StyleCapsule::ViewComponent)
29
- component_classes << klass
30
- end
31
- rescue
32
- # Skip this class if checking inheritance triggers ViewComponent loading errors
33
- # (e.g., ViewComponent 2.83.0 has a bug with Gem::Version#to_f)
34
- next
35
- end
36
- end
37
- rescue
38
- # ViewComponent may have loading issues (e.g., version compatibility)
39
- # Silently skip ViewComponent components if there's an error
40
- # This allows the rake task to continue with Phlex components
41
- end
42
-
43
- # Generate CSS files for each component
44
- component_classes.each do |component_class|
45
- next unless component_class.inline_cache_strategy == :file
46
- # Check for class method component_styles (required for file caching)
47
- next unless component_class.respond_to?(:component_styles, false)
48
-
49
- begin
50
- # Use class method component_styles for file caching
51
- css_content = component_class.component_styles
52
- next if css_content.nil? || css_content.to_s.strip.empty?
53
-
54
- # Create a temporary instance to get capsule
55
- # Some components might require arguments, so we catch errors
56
- instance = component_class.new
57
- capsule_id = instance.component_capsule
58
- scoped_css = instance.send(:scope_css, css_content)
59
-
60
- # Write CSS file
61
- file_path = StyleCapsule::CssFileWriter.write_css(
62
- css_content: scoped_css,
63
- component_class: component_class,
64
- capsule_id: capsule_id
65
- )
66
-
67
- puts "Generated: #{file_path}" if file_path
68
- rescue ArgumentError, NoMethodError => e
69
- # Component requires arguments or has dependencies - skip it
70
- puts "Skipped #{component_class.name}: #{e.message}"
71
- next
72
- end
73
- end
6
+ require "style_capsule/component_builder"
74
7
 
75
- puts "StyleCapsule CSS files built successfully"
8
+ StyleCapsule::ComponentBuilder.build_all(output_proc: ->(msg) { puts msg })
76
9
  end
77
10
 
78
11
  desc "Clear StyleCapsule generated CSS files"
@@ -88,7 +88,8 @@ module StyleCapsule
88
88
 
89
89
  module ViewComponentHelper
90
90
  def register_stylesheet: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
91
- def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> String
91
+ def stylesheet_registry_tags: (?namespace: Symbol | String | nil) -> String
92
+ def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> String # deprecated
92
93
  end
93
94
 
94
95
  module Helper
@@ -96,12 +97,14 @@ module StyleCapsule
96
97
  def scope_css: (String css_content, String capsule_id) -> String
97
98
  def style_capsule: (?String css_content, ?capsule_id: String | nil) { () -> String } -> String
98
99
  def register_stylesheet: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
99
- def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> String
100
+ def stylesheet_registry_tags: (?namespace: Symbol | String | nil) -> String
101
+ def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> String # deprecated
100
102
  end
101
103
 
102
104
  module PhlexHelper
103
105
  def register_stylesheet: (String file_path, ?namespace: Symbol | String | nil, **Hash[untyped, untyped] options) -> void
104
- def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> void
106
+ def stylesheet_registry_tags: (?namespace: Symbol | String | nil) -> void
107
+ def stylesheet_registrymap_tags: (?namespace: Symbol | String | nil) -> void # deprecated
105
108
  end
106
109
 
107
110
  class Railtie < Rails::Railtie