Module: StyleCapsule::StandaloneHelper

Includes:
HelperScopeCache
Defined in:
lib/style_capsule/standalone_helper.rb

Overview

Standalone helper module for use without Rails

This module provides basic HTML generation and CSS scoping functionality without requiring Rails ActionView helpers. It can be included in any framework's view context or used directly.

Examples:

Usage in Sinatra

class MyApp < Sinatra::Base
  helpers StyleCapsule::StandaloneHelper
end

Usage in plain Ruby

class MyView
  include StyleCapsule::StandaloneHelper
end

Usage in ERB (non-Rails)

# In your ERB template context
include StyleCapsule::StandaloneHelper
style_capsule do
  "<style>.section { color: red; }</style><div class='section'>Content</div>"
end

Constant Summary collapse

MAX_HTML_SIZE =

Maximum HTML content size (10MB) to prevent DoS attacks

10_000_000

Constants included from HelperScopeCache

HelperScopeCache::MAX_SCOPE_CACHE_ENTRIES, HelperScopeCache::STYLE_CLOSE, HelperScopeCache::STYLE_OPEN

Instance Method Summary collapse

Instance Method Details

#capture(&block) ⇒ String

Capture block content (simplified version without Rails)

Parameters:

  • block (Proc)

    Block to capture

Returns:

  • (String)

    Captured content



80
81
82
83
# File 'lib/style_capsule/standalone_helper.rb', line 80

def capture(&block)
  return "" unless block_given?
  block.call.to_s
end

#content_tag(tag, content = nil, **options, &block) ⇒ String

Generate HTML tag without Rails helpers

rubocop:disable Metrics/AbcSize -- serializes flat and nested HTML attributes

Parameters:

  • tag (String, Symbol)

    HTML tag name

  • content (String, nil) (defaults to: nil)

    Tag content (or use block)

  • options (Hash)

    HTML attributes

  • block (Proc)

    Block for tag content

Returns:

  • (String)

    HTML string



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/style_capsule/standalone_helper.rb', line 57

def (tag, content = nil, **options, &block)
  tag_name = tag.to_s
  content = capture(&block) if block_given? && content.nil?
  content ||= ""

  attrs = options.map do |k, v|
    if v.is_a?(Hash)
      # Handle nested attributes like data: { capsule: "abc" }
      v.map { |nk, nv| %(#{k}-#{nk}="#{escape_html_attr(nv)}") }.join(" ")
    else
      %(#{k}="#{escape_html_attr(v)}")
    end
  end.join(" ")

  attrs = " #{attrs}" unless attrs.empty?
  "<#{tag_name}#{attrs}>#{content}</#{tag_name}>"
end

#generate_capsule_id(css_content) ⇒ Object

Generate capsule ID based on caller location for uniqueness



37
38
39
40
41
42
# File 'lib/style_capsule/standalone_helper.rb', line 37

def generate_capsule_id(css_content)
  # Use caller location + CSS content for uniqueness
  caller_info = caller_locations(1, 1).first
  capsule_key = "#{caller_info.path}:#{caller_info.lineno}:#{css_content}"
  "a#{Digest::SHA1.hexdigest(capsule_key)}"[0, 8]
end

#html_safe(string) ⇒ String

Mark string as HTML-safe (for compatibility)

Parameters:

  • string (String)

    String to mark as safe

Returns:

  • (String)

    HTML-safe string



89
90
91
92
93
94
95
96
97
# File 'lib/style_capsule/standalone_helper.rb', line 89

def html_safe(string)
  # In non-Rails context, just return the string
  # If ActiveSupport is available, use its html_safe
  if string.respond_to?(:html_safe)
    string.html_safe
  else
    string
  end
end

#raw(string) ⇒ String

Raw string (no HTML escaping)

Parameters:

  • string (String)

    String to return as-is

Returns:

  • (String)

    Raw string



103
104
105
# File 'lib/style_capsule/standalone_helper.rb', line 103

def raw(string)
  html_safe(string.to_s)
end

#register_stylesheet(file_path, namespace: nil, **options) ⇒ void

This method returns an undefined value.

Register a stylesheet file for head rendering

Parameters:

  • file_path (String)

    Path to stylesheet

  • namespace (Symbol, String, nil) (defaults to: nil)

    Optional namespace

  • options (Hash)

    Options for stylesheet link tag



163
164
165
# File 'lib/style_capsule/standalone_helper.rb', line 163

def register_stylesheet(file_path, namespace: nil, **options)
  StyleCapsule::StylesheetRegistry.register(file_path, namespace: namespace, **options)
end

#scope_css(css_content, capsule_id) ⇒ Object

Scope CSS content and return scoped CSS



45
46
47
# File 'lib/style_capsule/standalone_helper.rb', line 45

def scope_css(css_content, capsule_id)
  scope_css_with_bounded_cache(css_content, capsule_id)
end

#style_capsule(css_content = nil, capsule_id: nil, tag: :div, &content_block) ⇒ String

ERB helper: automatically wraps content in scoped div and processes CSS

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- standalone helper mirrors Rails helper flow

Parameters:

  • css_content (String, nil) (defaults to: nil)

    CSS content (or extract from block)

  • capsule_id (String, nil) (defaults to: nil)

    Optional capsule ID

  • content_block (Proc)

    Block containing HTML content

Returns:

  • (String)

    HTML with scoped CSS and wrapped content



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/style_capsule/standalone_helper.rb', line 114

def style_capsule(css_content = nil, capsule_id: nil, tag: :div, &content_block)
  html_content = nil

  # If CSS content is provided as argument, use it
  # Otherwise, extract from content block
  if css_content.nil? && block_given?
    full_content = capture(&content_block)

    # Validate HTML content size to prevent DoS attacks
    if full_content.bytesize > MAX_HTML_SIZE
      raise ArgumentError, "HTML content exceeds maximum size of #{MAX_HTML_SIZE} bytes (got #{full_content.bytesize} bytes)"
    end

    html_content, css_content = extract_styles_from_markup(full_content)
    if css_content.nil?
      html_content = full_content
    end
  elsif css_content && block_given?
    html_content = capture(&content_block)
  elsif css_content && !block_given?
    # CSS provided but no content block - just return scoped CSS
    capsule_id ||= generate_capsule_id(css_content)
    scoped_css = scope_css(css_content, capsule_id)
    return (:style, raw(scoped_css), type: "text/css")
  else
    return ""
  end

  # If no CSS, just return content
  return html_safe(html_content) if css_content.nil? || css_content.to_s.strip.empty?

  # Use provided capsule_id or generate one
  capsule_id ||= generate_capsule_id(css_content)
  scoped_css = scope_css(css_content, capsule_id)

  # Render style tag and wrapped content
  style_tag = (:style, raw(scoped_css), type: "text/css")
  wrapped_content = (tag, raw(html_content), data: {capsule: capsule_id})

  html_safe(style_tag + wrapped_content)
end

#stylesheet_registry_tags(namespace: nil) ⇒ String

Render StyleCapsule registered stylesheets

Parameters:

  • namespace (Symbol, String, nil) (defaults to: nil)

    Optional namespace to render

Returns:

  • (String)

    HTML-safe string with stylesheet tags



171
172
173
# File 'lib/style_capsule/standalone_helper.rb', line 171

def stylesheet_registry_tags(namespace: nil)
  StyleCapsule::StylesheetRegistry.render_head_stylesheets(self, namespace: namespace)
end