Class: Rhales::View
- Inherits:
-
Object
- Object
- Rhales::View
- Defined in:
- lib/rhales/view.rb
Overview
Complete RSFC view implementation
Single public interface for RSFC template rendering that handles: - Context creation (with pluggable context classes) - Template loading and parsing - Template rendering with Rhales - Data hydration and injection
Context and Data Boundaries
Views implement a two-phase security model:
Server Templates: Full Context Access
Templates have complete access to all server-side data: - All props passed to View.new - Data from .rue file’s section (processed server-side) - Runtime data (CSRF tokens, nonces, request metadata) - Computed data (authentication status, theme classes) - User objects, configuration, internal APIs
Client Data: Explicit Allowlist
Only data declared in sections reahas_role?ches the browser: - Creates a REST API-like boundary - Server-side variable interpolation processes secrets safely - JSON serialization validates data structure - No accidental exposure of sensitive server data
Example: # Server template has full access: {user{user.admin?} {csrf_token} {internal_config}
# Client only gets declared data: { “user_name”: “{user{user.name}”, “theme”: “{user{user.theme}” }
See docs/CONTEXT_AND_DATA_BOUNDARIES.md for complete details.
Subclasses can override context_class to use different context implementations.
Defined Under Namespace
Classes: RenderError, TemplateNotFoundError
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#cust ⇒ Object
readonly
Returns the value of attribute cust.
-
#locale ⇒ Object
readonly
Returns the value of attribute locale.
-
#props ⇒ Object
readonly
Returns the value of attribute props.
-
#req ⇒ Object
readonly
Returns the value of attribute req.
-
#rsfc_context ⇒ Object
readonly
Returns the value of attribute rsfc_context.
-
#sess ⇒ Object
readonly
Returns the value of attribute sess.
Class Method Summary collapse
-
.default_template_name ⇒ Object
Get default template name based on class name.
-
.render_with_data(req, sess, cust, locale, template_name: nil, config: nil, **props) ⇒ Object
Render template with props.
-
.with_data(req, sess, cust, locale, config: nil, **props) ⇒ Object
Create view instance with props.
Instance Method Summary collapse
-
#build_view_composition(template_name) ⇒ Object
private
Build view composition for the given template.
-
#calculate_etag(template_name = nil, additional_context = {}) ⇒ Object
Calculate ETag for current template data.
-
#context_class ⇒ Object
protected
Return the context class to use Subclasses can override this to use different context implementations.
-
#create_context ⇒ Object
protected
Create the appropriate context for this view Subclasses can override this to use different context types.
-
#create_context_with_rue_data(parser) ⇒ Object
private
Create context that includes data from .rue file’s data section.
-
#create_partial_resolver ⇒ Object
private
Create partial resolver for partial} inclusions.
-
#create_partial_resolver_from_composition(composition) ⇒ Object
private
Create partial resolver that uses pre-loaded templates from composition.
-
#data_changed?(template_name = nil, etag = nil, additional_context = {}) ⇒ Boolean
Check if template data has changed for caching.
-
#data_hash(template_name = nil) ⇒ Object
Get processed data as hash (for API endpoints or testing).
-
#detect_mount_point_in_rendered_html(template_html) ⇒ Object
private
Detect mount points in fully rendered HTML.
-
#extract_rue_data(parser) ⇒ Object
private
Extract and process data from .rue file’s data section.
-
#generate_hydration(parser) ⇒ Object
private
Generate data hydration HTML.
-
#generate_hydration_from_merged_data(merged_data) ⇒ Object
private
Generate hydration HTML from pre-merged data.
-
#generate_reflection_utilities ⇒ Object
private
Generate JavaScript utilities for hydration reflection.
-
#initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil) ⇒ View
constructor
A new instance of View.
-
#inject_hydration_into_template(template_html, hydration_html) ⇒ Object
private
Legacy injection method (kept for backwards compatibility).
-
#inject_hydration_with_mount_points(composition, template_name, template_html, hydration_html) ⇒ Object
private
Smart hydration injection with mount point detection on rendered HTML.
-
#load_template(template_name) ⇒ Object
private
Load and parse template.
-
#load_template_for_composition(template_name) ⇒ Object
private
Loader proc for ViewComposition.
-
#nonce_attribute ⇒ Object
private
Get nonce attribute if available.
-
#reflection_enabled? ⇒ Boolean
private
Check if reflection system is enabled.
-
#render(template_name = nil) ⇒ Object
Render RSFC template with hydration using two-pass architecture.
-
#render_hydration_only(template_name = nil) ⇒ Object
Generate only the data hydration HTML.
-
#render_json_only(template_name = nil, additional_context = {}) ⇒ Object
Render JSON response for API endpoints (link-based strategies).
-
#render_jsonp_only(template_name = nil, callback_name = 'callback', additional_context = {}) ⇒ Object
Render JSONP response with callback.
-
#render_module_only(template_name = nil, additional_context = {}) ⇒ Object
Render ES module response for modulepreload strategy.
-
#render_template_only(template_name = nil) ⇒ Object
Render only the template section (without data hydration).
-
#render_template_section(parser) ⇒ Object
private
Render template section with Rhales.
-
#render_template_with_composition(composition, root_template_name) ⇒ Object
private
Render template using the view composition.
-
#resolve_template_path(template_name) ⇒ Object
private
Resolve template path.
-
#set_csp_header_if_enabled ⇒ Object
private
Set CSP header if enabled.
-
#templates_root ⇒ Object
private
Get templates root directory.
Constructor Details
#initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil) ⇒ View
Returns a new instance of View.
59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/rhales/view.rb', line 59 def initialize(req, sess = nil, cust = nil, locale_override = nil, props: {}, config: nil) @req = req @sess = sess @cust = cust @locale = locale_override @props = props @config = config || Rhales.configuration # Create context using the specified context class @rsfc_context = create_context end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
57 58 59 |
# File 'lib/rhales/view.rb', line 57 def config @config end |
#cust ⇒ Object (readonly)
Returns the value of attribute cust.
57 58 59 |
# File 'lib/rhales/view.rb', line 57 def cust @cust end |
#locale ⇒ Object (readonly)
Returns the value of attribute locale.
57 58 59 |
# File 'lib/rhales/view.rb', line 57 def locale @locale end |
#props ⇒ Object (readonly)
Returns the value of attribute props.
57 58 59 |
# File 'lib/rhales/view.rb', line 57 def props @props end |
#req ⇒ Object (readonly)
Returns the value of attribute req.
57 58 59 |
# File 'lib/rhales/view.rb', line 57 def req @req end |
#rsfc_context ⇒ Object (readonly)
Returns the value of attribute rsfc_context.
57 58 59 |
# File 'lib/rhales/view.rb', line 57 def rsfc_context @rsfc_context end |
#sess ⇒ Object (readonly)
Returns the value of attribute sess.
57 58 59 |
# File 'lib/rhales/view.rb', line 57 def sess @sess end |
Class Method Details
.default_template_name ⇒ Object
Get default template name based on class name
531 532 533 534 535 536 537 538 |
# File 'lib/rhales/view.rb', line 531 def default_template_name # Convert ClassName to class_name name.split('::').last .gsub(/([A-Z])/, '_\1') .downcase .sub(/^_/, '') .sub(/_view$/, '') end |
.render_with_data(req, sess, cust, locale, template_name: nil, config: nil, **props) ⇒ Object
Render template with props
541 542 543 544 |
# File 'lib/rhales/view.rb', line 541 def render_with_data(req, sess, cust, locale, template_name: nil, config: nil, **props) view = new(req, sess, cust, locale, props: props, config: config) view.render(template_name) end |
.with_data(req, sess, cust, locale, config: nil, **props) ⇒ Object
Create view instance with props
547 548 549 |
# File 'lib/rhales/view.rb', line 547 def with_data(req, sess, cust, locale, config: nil, **props) new(req, sess, cust, locale, props: props, config: config) end |
Instance Method Details
#build_view_composition(template_name) ⇒ Object (private)
Build view composition for the given template
343 344 345 346 347 |
# File 'lib/rhales/view.rb', line 343 def build_view_composition(template_name) loader = method(:load_template_for_composition) composition = ViewComposition.new(template_name, loader: loader, config: @config) composition.resolve! end |
#calculate_etag(template_name = nil, additional_context = {}) ⇒ Object
Calculate ETag for current template data
142 143 144 145 146 147 148 |
# File 'lib/rhales/view.rb', line 142 def calculate_etag(template_name = nil, additional_context = {}) require_relative 'hydration_endpoint' template_name ||= self.class.default_template_name endpoint = HydrationEndpoint.new(@config, @rsfc_context) endpoint.calculate_etag(template_name, additional_context) end |
#context_class ⇒ Object (protected)
Return the context class to use Subclasses can override this to use different context implementations
183 184 185 |
# File 'lib/rhales/view.rb', line 183 def context_class Context end |
#create_context ⇒ Object (protected)
Create the appropriate context for this view Subclasses can override this to use different context types
177 178 179 |
# File 'lib/rhales/view.rb', line 177 def create_context context_class.for_view(@req, @sess, @cust, @locale, config: @config, **@props) end |
#create_context_with_rue_data(parser) ⇒ Object (private)
Create context that includes data from .rue file’s data section
277 278 279 280 281 282 283 284 285 286 |
# File 'lib/rhales/view.rb', line 277 def create_context_with_rue_data(parser) # Get data from .rue file's data section rue_data = extract_rue_data(parser) # Merge rue data with existing props (rue data takes precedence) merged_props = @props.merge(rue_data) # Create new context with merged data context_class.for_view(@req, @sess, @cust, @locale, config: @config, **merged_props) end |
#create_partial_resolver ⇒ Object (private)
Create partial resolver for partial} inclusions
257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/rhales/view.rb', line 257 def create_partial_resolver templates_dir = File.join(templates_root, 'web') proc do |partial_name| partial_path = File.join(templates_dir, "#{partial_name}.rue") if File.exist?(partial_path) # Return full partial content so TemplateEngine can process # data sections, otherwise nil. File.read(partial_path) end end end |
#create_partial_resolver_from_composition(composition) ⇒ Object (private)
Create partial resolver that uses pre-loaded templates from composition
400 401 402 403 404 405 |
# File 'lib/rhales/view.rb', line 400 def create_partial_resolver_from_composition(composition) proc do |partial_name| parser = composition.template(partial_name) parser ? parser.content : nil end end |
#data_changed?(template_name = nil, etag = nil, additional_context = {}) ⇒ Boolean
Check if template data has changed for caching
133 134 135 136 137 138 139 |
# File 'lib/rhales/view.rb', line 133 def data_changed?(template_name = nil, etag = nil, additional_context = {}) require_relative 'hydration_endpoint' template_name ||= self.class.default_template_name endpoint = HydrationEndpoint.new(@config, @rsfc_context) endpoint.data_changed?(template_name, etag, additional_context) end |
#data_hash(template_name = nil) ⇒ Object
Get processed data as hash (for API endpoints or testing)
164 165 166 167 168 169 170 171 |
# File 'lib/rhales/view.rb', line 164 def data_hash(template_name = nil) template_name ||= self.class.default_template_name # Build composition and aggregate data composition = build_view_composition(template_name) aggregator = HydrationDataAggregator.new(@rsfc_context) aggregator.aggregate(composition) end |
#detect_mount_point_in_rendered_html(template_html) ⇒ Object (private)
Detect mount points in fully rendered HTML
334 335 336 337 338 339 340 |
# File 'lib/rhales/view.rb', line 334 def detect_mount_point_in_rendered_html(template_html) return nil unless @config&.hydration custom_selectors = @config.hydration.mount_point_selectors || [] detector = MountPointDetector.new detector.detect(template_html, custom_selectors) end |
#extract_rue_data(parser) ⇒ Object (private)
Extract and process data from .rue file’s data section
289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/rhales/view.rb', line 289 def extract_rue_data(parser) data_content = parser.section('data') return {} unless data_content # Process the data section as JSON and parse it hydrator = Hydrator.new(parser, @rsfc_context) hydrator.processed_data_hash rescue JSON::ParserError, Hydrator::JSONSerializationError => ex puts "Error processing data section: #{ex.}" # If data section isn't valid JSON, return empty hash # This allows templates to work even with malformed data sections {} end |
#generate_hydration(parser) ⇒ Object (private)
Generate data hydration HTML
272 273 274 |
# File 'lib/rhales/view.rb', line 272 def generate_hydration(parser) Hydrator.generate(parser, @rsfc_context) end |
#generate_hydration_from_merged_data(merged_data) ⇒ Object (private)
Generate hydration HTML from pre-merged data
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 |
# File 'lib/rhales/view.rb', line 408 def generate_hydration_from_merged_data(merged_data) hydration_parts = [] merged_data.each do |window_attr, data| # Generate unique ID for this data block unique_id = "rsfc-data-#{SecureRandom.hex(8)}" # Create JSON script tag with optional reflection attributes json_attrs = reflection_enabled? ? " data-window=\"#{window_attr}\"" : "" json_script = <<~HTML.strip <script id="#{unique_id}" type="application/json"#{json_attrs}>#{JSON.generate(data)}</script> HTML # Create hydration script with optional reflection attributes nonce_attr = nonce_attribute hydration_attrs = reflection_enabled? ? " data-hydration-target=\"#{window_attr}\"" : "" hydration_script = if reflection_enabled? <<~HTML.strip <script#{nonce_attr}#{hydration_attrs}> var dataScript = document.getElementById('#{unique_id}'); var targetName = dataScript.getAttribute('data-window') || '#{window_attr}'; window[targetName] = JSON.parse(dataScript.textContent); </script> HTML else <<~HTML.strip <script#{nonce_attr}#{hydration_attrs}> window['#{window_attr}'] = JSON.parse(document.getElementById('#{unique_id}').textContent); </script> HTML end hydration_parts << json_script hydration_parts << hydration_script end # Add reflection utilities if enabled if reflection_enabled? && !merged_data.empty? hydration_parts << generate_reflection_utilities end hydration_parts.join("\n") end |
#generate_reflection_utilities ⇒ Object (private)
Generate JavaScript utilities for hydration reflection
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 |
# File 'lib/rhales/view.rb', line 458 def generate_reflection_utilities nonce_attr = nonce_attribute <<~HTML.strip <script#{nonce_attr}> // Rhales hydration reflection utilities window.__rhales__ = window.__rhales__ || { getHydrationTargets: function() { return Array.from(document.querySelectorAll('[data-hydration-target]')); }, getDataForTarget: function(target) { var targetName = target.dataset.hydrationTarget; return targetName ? window[targetName] : undefined; }, getWindowAttribute: function(scriptEl) { return scriptEl.dataset.window; }, getDataScripts: function() { return Array.from(document.querySelectorAll('script[data-window]')); }, refreshData: function(target) { var targetName = target.dataset.hydrationTarget; var dataScript = document.querySelector('script[data-window="' + targetName + '"]'); if (dataScript && targetName) { try { window[targetName] = JSON.parse(dataScript.textContent); return true; } catch (e) { console.error('Rhales: Failed to refresh data for ' + targetName, e); return false; } } return false; }, getAllHydrationData: function() { var data = {}; this.getHydrationTargets().forEach(function(target) { var targetName = target.dataset.hydrationTarget; if (targetName) { data[targetName] = window[targetName]; } }); return data; } }; </script> HTML end |
#inject_hydration_into_template(template_html, hydration_html) ⇒ Object (private)
Legacy injection method (kept for backwards compatibility)
323 324 325 326 327 328 329 330 331 |
# File 'lib/rhales/view.rb', line 323 def inject_hydration_into_template(template_html, hydration_html) # Try to inject before closing </body> tag if template_html.include?('</body>') template_html.sub('</body>', "#{hydration_html}\n</body>") # Otherwise append to end else "#{template_html}\n#{hydration_html}" end end |
#inject_hydration_with_mount_points(composition, template_name, template_html, hydration_html) ⇒ Object (private)
Smart hydration injection with mount point detection on rendered HTML
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/rhales/view.rb', line 304 def inject_hydration_with_mount_points(composition, template_name, template_html, hydration_html) injector = HydrationInjector.new(@config.hydration, template_name) # Check if using link-based strategy if @config.hydration.link_based_strategy? # For link-based strategies, we need the merged data context aggregator = HydrationDataAggregator.new(@rsfc_context) merged_data = aggregator.aggregate(composition) nonce = @rsfc_context.get('nonce') injector.inject_link_based_strategy(template_html, merged_data, nonce) else # Traditional strategies (early, earliest, late) mount_point = detect_mount_point_in_rendered_html(template_html) injector.inject(template_html, hydration_html, mount_point) end end |
#load_template(template_name) ⇒ Object (private)
Load and parse template
190 191 192 193 194 195 196 197 198 199 |
# File 'lib/rhales/view.rb', line 190 def load_template(template_name) template_path = resolve_template_path(template_name) unless File.exist?(template_path) raise TemplateNotFoundError, "Template not found: #{template_path}" end # Use refinement to load .rue file require template_path end |
#load_template_for_composition(template_name) ⇒ Object (private)
Loader proc for ViewComposition
350 351 352 353 354 355 356 357 |
# File 'lib/rhales/view.rb', line 350 def load_template_for_composition(template_name) template_path = resolve_template_path(template_name) return nil unless File.exist?(template_path) require template_path rescue StandardError => ex raise TemplateNotFoundError, "Failed to load template #{template_name}: #{ex.}" end |
#nonce_attribute ⇒ Object (private)
Get nonce attribute if available
508 509 510 511 |
# File 'lib/rhales/view.rb', line 508 def nonce_attribute nonce = @rsfc_context.get('nonce') nonce ? " nonce=\"#{ERB::Util.html_escape(nonce)}\"" : '' end |
#reflection_enabled? ⇒ Boolean (private)
Check if reflection system is enabled
453 454 455 |
# File 'lib/rhales/view.rb', line 453 def reflection_enabled? @config.hydration.reflection_enabled end |
#render(template_name = nil) ⇒ Object
Render RSFC template with hydration using two-pass architecture
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/rhales/view.rb', line 72 def render(template_name = nil) template_name ||= self.class.default_template_name # Phase 1: Build view composition and aggregate data composition = build_view_composition(template_name) aggregator = HydrationDataAggregator.new(@rsfc_context) merged_hydration_data = aggregator.aggregate(composition) # Phase 2: Render HTML with pre-computed data # Render template content template_html = render_template_with_composition(composition, template_name) # Generate hydration HTML with merged data hydration_html = generate_hydration_from_merged_data(merged_hydration_data) # Set CSP header if enabled set_csp_header_if_enabled # Smart hydration injection with mount point detection inject_hydration_with_mount_points(composition, template_name, template_html, hydration_html) rescue StandardError => ex raise RenderError, "Failed to render template '#{template_name}': #{ex.}" end |
#render_hydration_only(template_name = nil) ⇒ Object
Generate only the data hydration HTML
151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/rhales/view.rb', line 151 def render_hydration_only(template_name = nil) template_name ||= self.class.default_template_name # Build composition and aggregate data composition = build_view_composition(template_name) aggregator = HydrationDataAggregator.new(@rsfc_context) merged_hydration_data = aggregator.aggregate(composition) # Generate hydration HTML generate_hydration_from_merged_data(merged_hydration_data) end |
#render_json_only(template_name = nil, additional_context = {}) ⇒ Object
Render JSON response for API endpoints (link-based strategies)
106 107 108 109 110 111 112 |
# File 'lib/rhales/view.rb', line 106 def render_json_only(template_name = nil, additional_context = {}) require_relative 'hydration_endpoint' template_name ||= self.class.default_template_name endpoint = HydrationEndpoint.new(@config, @rsfc_context) endpoint.render_json(template_name, additional_context) end |
#render_jsonp_only(template_name = nil, callback_name = 'callback', additional_context = {}) ⇒ Object
Render JSONP response with callback
124 125 126 127 128 129 130 |
# File 'lib/rhales/view.rb', line 124 def render_jsonp_only(template_name = nil, callback_name = 'callback', additional_context = {}) require_relative 'hydration_endpoint' template_name ||= self.class.default_template_name endpoint = HydrationEndpoint.new(@config, @rsfc_context) endpoint.render_jsonp(template_name, callback_name, additional_context) end |
#render_module_only(template_name = nil, additional_context = {}) ⇒ Object
Render ES module response for modulepreload strategy
115 116 117 118 119 120 121 |
# File 'lib/rhales/view.rb', line 115 def render_module_only(template_name = nil, additional_context = {}) require_relative 'hydration_endpoint' template_name ||= self.class.default_template_name endpoint = HydrationEndpoint.new(@config, @rsfc_context) endpoint.render_module(template_name, additional_context) end |
#render_template_only(template_name = nil) ⇒ Object
Render only the template section (without data hydration)
97 98 99 100 101 102 103 |
# File 'lib/rhales/view.rb', line 97 def render_template_only(template_name = nil) template_name ||= self.class.default_template_name # Build composition for consistent behavior composition = build_view_composition(template_name) render_template_with_composition(composition, template_name) end |
#render_template_section(parser) ⇒ Object (private)
Render template section with Rhales
RSFC Security Model: Templates have full server context access - Templates can access all business data, user objects, methods, etc. - Templates can access data from .rue file’s section (processed server-side) - This is like any server-side template (ERB, HAML, etc.) - Security boundary is at server-to-client handoff, not within server rendering - Only data declared in section reaches the client (after processing)
242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/rhales/view.rb', line 242 def render_template_section(parser) template_content = parser.section('template') return '' unless template_content # Create partial resolver partial_resolver = create_partial_resolver # Merge .rue file data with existing context context_with_rue_data = create_context_with_rue_data(parser) # Render with full server context (props + computed context + rue data) TemplateEngine.render(template_content, context_with_rue_data, partial_resolver: partial_resolver) end |
#render_template_with_composition(composition, root_template_name) ⇒ Object (private)
Render template using the view composition
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/rhales/view.rb', line 360 def render_template_with_composition(composition, root_template_name) root_parser = composition.template(root_template_name) template_content = root_parser.section('template') return '' unless template_content # Create partial resolver that uses the composition partial_resolver = create_partial_resolver_from_composition(composition) # Merge .rue file data with existing context context_with_rue_data = create_context_with_rue_data(root_parser) # Check if template has a layout if root_parser.layout && composition.template(root_parser.layout) # Render content template first content_html = TemplateEngine.render(template_content, context_with_rue_data, partial_resolver: partial_resolver) # Then render layout with content layout_parser = composition.template(root_parser.layout) layout_content = layout_parser.section('template') return '' unless layout_content # Create new context with content for layout rendering layout_props = context_with_rue_data.props.merge('content' => content_html) layout_context = Context.new( context_with_rue_data.req, context_with_rue_data.sess, context_with_rue_data.cust, context_with_rue_data.locale, props: layout_props, config: context_with_rue_data.config, ) TemplateEngine.render(layout_content, layout_context, partial_resolver: partial_resolver) else # Render with full server context (no layout) TemplateEngine.render(template_content, context_with_rue_data, partial_resolver: partial_resolver) end end |
#resolve_template_path(template_name) ⇒ Object (private)
Resolve template path
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/rhales/view.rb', line 202 def resolve_template_path(template_name) # Check configured template paths first if @config && @config.template_paths && !@config.template_paths.empty? @config.template_paths.each do |path| template_path = File.join(path, "#{template_name}.rue") return template_path if File.exist?(template_path) end end # Fallback to default template structure # First try templates/web directory web_path = File.join(templates_root, 'web', "#{template_name}.rue") return web_path if File.exist?(web_path) # Then try templates directory templates_path = File.join(templates_root, "#{template_name}.rue") return templates_path if File.exist?(templates_path) # Return first configured path or web path for error message if @config && @config.template_paths && !@config.template_paths.empty? File.join(@config.template_paths.first, "#{template_name}.rue") else web_path end end |
#set_csp_header_if_enabled ⇒ Object (private)
Set CSP header if enabled
514 515 516 517 518 519 520 521 522 523 524 525 526 527 |
# File 'lib/rhales/view.rb', line 514 def set_csp_header_if_enabled return unless @config.csp_enabled return unless @req && @req.respond_to?(:env) # Get nonce from context nonce = @rsfc_context.get('nonce') # Create CSP instance and build header csp = CSP.new(@config, nonce: nonce) header_value = csp.build_header # Set header in request environment for framework to use @req.env['csp_header'] = header_value if header_value end |
#templates_root ⇒ Object (private)
Get templates root directory
229 230 231 232 |
# File 'lib/rhales/view.rb', line 229 def templates_root boot_root = File.('../../..', __dir__) File.join(boot_root, 'templates') end |