Class: Rhales::TemplateEngine
- Inherits:
-
Object
- Object
- Rhales::TemplateEngine
- Includes:
- Utils::LoggingHelpers
- Defined in:
- lib/rhales/core/template_engine.rb
Overview
Rhales - Ruby Handlebars-style template engine
Modern AST-based template engine that supports both simple template strings and full .rue files. Uses RueFormatParser for parsing with proper nested structure handling and block statement support.
Features: - Dual-mode operation: simple templates and .rue files - Full AST parsing eliminates regex-based vulnerabilities - Proper nested block handling with accurate error reporting - XSS protection through HTML escaping by default - Handlebars-compatible syntax with Ruby idioms
Supported syntax: - {variable} - Variable interpolation with HTML escaping - {{variable}} - Raw variable interpolation (no escaping) - condition} … {else} … {/if} - Conditionals with else - condition} … {/unless} - Negated conditionals - items} … {/each} - Iteration with context - partial_name} - Partial inclusion
Defined Under Namespace
Classes: BlockNotFoundError, EachContext, PartialNotFoundError, RenderError, UndefinedVariableError
Instance Attribute Summary collapse
-
#context ⇒ Object
readonly
Returns the value of attribute context.
-
#parser ⇒ Object
readonly
Returns the value of attribute parser.
-
#partial_resolver ⇒ Object
readonly
Returns the value of attribute partial_resolver.
-
#template_content ⇒ Object
readonly
Returns the value of attribute template_content.
Class Method Summary collapse
-
.file_partial_resolver(templates_dir) ⇒ Object
Create partial resolver that loads .rue files from a directory.
-
.render(template_content, context, partial_resolver: nil) ⇒ Object
Render template with context and optional partial resolver.
Instance Method Summary collapse
-
#create_each_context(item, index, items_var) ⇒ Object
private
Create context for each iteration.
-
#escape_html(string) ⇒ Object
private
HTML escape for XSS protection.
-
#evaluate_condition(condition) ⇒ Object
private
Evaluate condition for if/unless blocks.
-
#get_variable_value(variable_name) ⇒ Object
private
Get variable value from context.
-
#initialize(template_content, context, partial_resolver: nil) ⇒ TemplateEngine
constructor
A new instance of TemplateEngine.
-
#partials ⇒ Object
Get all partials used in the template.
-
#render ⇒ Object
-
#render_content_nodes(content_nodes) ⇒ Object
private
Render array of AST content nodes with proper block handling Processes text nodes and AST block nodes directly.
-
#render_each_block(node) ⇒ Object
private
-
#render_handlebars_expression(node) ⇒ Object
private
-
#render_if_block(node) ⇒ Object
private
-
#render_partial(partial_name) ⇒ Object
private
-
#render_partial_expression(node) ⇒ Object
private
-
#render_template_string(template_string) ⇒ Object
private
-
#render_unless_block(node) ⇒ Object
private
-
#render_variable_expression(node) ⇒ Object
private
-
#schema_path ⇒ Object
Access schema path from parsed .rue file.
-
#simple_template? ⇒ Boolean
private
-
#template_variables ⇒ Object
Get template variables used in the template.
-
#window_attribute ⇒ Object
Access window attribute from parsed .rue file.
Methods included from Utils::LoggingHelpers
#format_value, #log_timed_operation, #log_with_metadata
Methods included from Utils
Constructor Details
#initialize(template_content, context, partial_resolver: nil) ⇒ TemplateEngine
Returns a new instance of TemplateEngine.
40 41 42 43 44 45 |
# File 'lib/rhales/core/template_engine.rb', line 40 def initialize(template_content, context, partial_resolver: nil) @template_content = template_content @context = context @partial_resolver = partial_resolver @parser = nil end |
Instance Attribute Details
#context ⇒ Object (readonly)
Returns the value of attribute context.
38 39 40 |
# File 'lib/rhales/core/template_engine.rb', line 38 def context @context end |
#parser ⇒ Object (readonly)
Returns the value of attribute parser.
38 39 40 |
# File 'lib/rhales/core/template_engine.rb', line 38 def parser @parser end |
#partial_resolver ⇒ Object (readonly)
Returns the value of attribute partial_resolver.
38 39 40 |
# File 'lib/rhales/core/template_engine.rb', line 38 def partial_resolver @partial_resolver end |
#template_content ⇒ Object (readonly)
Returns the value of attribute template_content.
38 39 40 |
# File 'lib/rhales/core/template_engine.rb', line 38 def template_content @template_content end |
Class Method Details
.file_partial_resolver(templates_dir) ⇒ Object
Create partial resolver that loads .rue files from a directory
379 380 381 382 383 384 385 386 387 388 389 |
# File 'lib/rhales/core/template_engine.rb', line 379 def file_partial_resolver(templates_dir) proc do |partial_name| partial_path = File.join(templates_dir, "#{partial_name}.rue") if File.exist?(partial_path) # Load and parse the partial .rue file document = RueDocument.parse_file(partial_path) document.section('template') end end end |
.render(template_content, context, partial_resolver: nil) ⇒ Object
Render template with context and optional partial resolver
374 375 376 |
# File 'lib/rhales/core/template_engine.rb', line 374 def render(template_content, context, partial_resolver: nil) new(template_content, context, partial_resolver: partial_resolver).render end |
Instance Method Details
#create_each_context(item, index, items_var) ⇒ Object (private)
Create context for each iteration
307 308 309 |
# File 'lib/rhales/core/template_engine.rb', line 307 def create_each_context(item, index, items_var) EachContext.new(@context, item, index, items_var) end |
#escape_html(string) ⇒ Object (private)
HTML escape for XSS protection
312 313 314 |
# File 'lib/rhales/core/template_engine.rb', line 312 def escape_html(string) ERB::Util.html_escape(string) end |
#evaluate_condition(condition) ⇒ Object (private)
Evaluate condition for if/unless blocks
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/rhales/core/template_engine.rb', line 284 def evaluate_condition(condition) value = get_variable_value(condition) # Handle truthy/falsy evaluation case value when nil, false false when '' false when 'false', 'False', 'FALSE' false when Array !value.empty? when Hash !value.empty? when 0 false else true end end |
#get_variable_value(variable_name) ⇒ Object (private)
Get variable value from context
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/rhales/core/template_engine.rb', line 266 def get_variable_value(variable_name) # Handle special variables case variable_name when 'this', '.' return @context.respond_to?(:current_item) ? @context.current_item : nil when '@index' return @context.respond_to?(:current_index) ? @context.current_index : nil end # Get from context if @context.respond_to?(:get) @context.get(variable_name) elsif @context.respond_to?(:[]) @context[variable_name] || @context[variable_name.to_sym] end end |
#partials ⇒ Object
Get all partials used in the template
107 108 109 |
# File 'lib/rhales/core/template_engine.rb', line 107 def partials @parser&.partials || [] end |
#render ⇒ Object
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/rhales/core/template_engine.rb', line 47 def render template_type = simple_template? ? :handlebars : :rue log_timed_operation(Rhales.logger, :debug, 'Template compiled', template_type: template_type, cached: false ) do # Check if this is a simple template or a full .rue file if simple_template? # Use HandlebarsParser for simple templates parser = HandlebarsParser.new(@template_content) parser.parse! render_content_nodes(parser.ast.children) else # Use RueDocument for .rue files @parser = RueDocument.new(@template_content) @parser.parse! # Get template section via RueDocument template_content = @parser.section('template') raise RenderError, 'Missing template section' unless template_content # Render the template section as a simple template render_template_string(template_content) end end rescue ::Rhales::ParseError => ex # Parse errors already have good error messages with location (Rhales.logger, :error, 'Template parse error', error: ex., line: ex.line, column: ex.column, section: ex.source_type ) raise RenderError, "Template parsing failed: #{ex.}" rescue ::Rhales::ValidationError => ex # Validation errors from RueDocument (Rhales.logger, :error, 'Template validation error', error: ex., template_type: :rue ) raise RenderError, "Template validation failed: #{ex.}" rescue StandardError => ex (Rhales.logger, :error, 'Template render error', error: ex., error_class: ex.class.name ) raise RenderError, "Template rendering failed: #{ex.}" end |
#render_content_nodes(content_nodes) ⇒ Object (private)
Render array of AST content nodes with proper block handling Processes text nodes and AST block nodes directly
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 |
# File 'lib/rhales/core/template_engine.rb', line 126 def render_content_nodes(content_nodes) return '' unless content_nodes.is_a?(Array) result = '' content_nodes.each do |node| case node.type when :text result += node.value when :variable_expression result += render_variable_expression(node) when :partial_expression result += render_partial_expression(node) when :if_block result += render_if_block(node) when :unless_block result += render_unless_block(node) when :each_block result += render_each_block(node) when :handlebars_expression # Handle handlebars expressions result += (node) end end result end |
#render_each_block(node) ⇒ Object (private)
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/rhales/core/template_engine.rb', line 198 def render_each_block(node) items_var = node.value[:items] block_content = node.value[:content] items = get_variable_value(items_var) if items.respond_to?(:each) items.map.with_index do |item, index| # Create context for each iteration item_context = create_each_context(item, index, items_var) engine = self.class.new('', item_context, partial_resolver: @partial_resolver) engine.send(:render_content_nodes, block_content) end.join else '' end end |
#render_handlebars_expression(node) ⇒ Object (private)
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/rhales/core/template_engine.rb', line 216 def (node) content = node.value[:content] raw = node.value[:raw] # Handle different expression types case content when /^>\s*(\w+)/ # Partials render_partial(Regexp.last_match(1)) when %r{^(#|/)(if|unless|each)} # Block statements (should be handled by render_content_nodes) '' else # Variables value = get_variable_value(content) if raw (Rhales.logger, :warn, 'Unescaped variable usage', variable: content, value_type: value.class.name, template_context: 'handlebars_expression' ) value.to_s else escape_html(value.to_s) end end end |
#render_if_block(node) ⇒ Object (private)
175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/rhales/core/template_engine.rb', line 175 def render_if_block(node) condition = node.value[:condition] if_content = node.value[:if_content] else_content = node.value[:else_content] if evaluate_condition(condition) render_content_nodes(if_content) else render_content_nodes(else_content) end end |
#render_partial(partial_name) ⇒ Object (private)
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'lib/rhales/core/template_engine.rb', line 239 def render_partial(partial_name) return "{{> #{partial_name}}}" unless @partial_resolver partial_content = @partial_resolver.call(partial_name) raise PartialNotFoundError, "Partial '#{partial_name}' not found" unless partial_content # Check if this is a .rue document with sections if partial_content.match?(/^<(schema|template|logic)\b/) # Parse as RueDocument to handle schema sections properly partial_doc = RueDocument.new(partial_content) partial_doc.parse! # Extract template section template_content = partial_doc.section('template') raise PartialNotFoundError, "Partial '#{partial_name}' missing template section" unless template_content # Render template with current context engine = self.class.new(template_content, @context, partial_resolver: @partial_resolver) engine.render else # Simple template without sections - render as before engine = self.class.new(partial_content, @context, partial_resolver: @partial_resolver) engine.render end end |
#render_partial_expression(node) ⇒ Object (private)
170 171 172 173 |
# File 'lib/rhales/core/template_engine.rb', line 170 def render_partial_expression(node) partial_name = node.value[:name] render_partial(partial_name) end |
#render_template_string(template_string) ⇒ Object (private)
117 118 119 120 121 122 |
# File 'lib/rhales/core/template_engine.rb', line 117 def render_template_string(template_string) # Parse the template string as a simple Handlebars template parser = HandlebarsParser.new(template_string) parser.parse! render_content_nodes(parser.ast.children) end |
#render_unless_block(node) ⇒ Object (private)
187 188 189 190 191 192 193 194 195 196 |
# File 'lib/rhales/core/template_engine.rb', line 187 def render_unless_block(node) condition = node.value[:condition] content = node.value[:content] if evaluate_condition(condition) '' else render_content_nodes(content) end end |
#render_variable_expression(node) ⇒ Object (private)
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/rhales/core/template_engine.rb', line 154 def render_variable_expression(node) name = node.value[:name] raw = node.value[:raw] value = get_variable_value(name) if raw (Rhales.logger, :warn, 'Unescaped variable usage', variable: name, value_type: value.class.name, template_context: 'variable_expression' ) value.to_s else escape_html(value.to_s) end end |
#schema_path ⇒ Object
Access schema path from parsed .rue file
97 98 99 |
# File 'lib/rhales/core/template_engine.rb', line 97 def schema_path @parser&.schema_path end |
#simple_template? ⇒ Boolean (private)
113 114 115 |
# File 'lib/rhales/core/template_engine.rb', line 113 def simple_template? !@template_content.match?(/^<(schema|template|logic)\b/) end |
#template_variables ⇒ Object
Get template variables used in the template
102 103 104 |
# File 'lib/rhales/core/template_engine.rb', line 102 def template_variables @parser&.template_variables || [] end |
#window_attribute ⇒ Object
Access window attribute from parsed .rue file
92 93 94 |
# File 'lib/rhales/core/template_engine.rb', line 92 def window_attribute @parser&.window_attribute end |