Class: Rhales::TemplateEngine
- Inherits:
-
Object
- Object
- Rhales::TemplateEngine
- Defined in:
- lib/rhales/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.
-
#create_merged_context(parent_context, local_data) ⇒ Object
private
Create a new context with merged data.
-
#data_attributes ⇒ Object
Access all data attributes from parsed .rue file.
-
#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.
Constructor Details
#initialize(template_content, context, partial_resolver: nil) ⇒ TemplateEngine
Returns a new instance of TemplateEngine.
38 39 40 41 42 43 |
# File 'lib/rhales/template_engine.rb', line 38 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.
36 37 38 |
# File 'lib/rhales/template_engine.rb', line 36 def context @context end |
#parser ⇒ Object (readonly)
Returns the value of attribute parser.
36 37 38 |
# File 'lib/rhales/template_engine.rb', line 36 def parser @parser end |
#partial_resolver ⇒ Object (readonly)
Returns the value of attribute partial_resolver.
36 37 38 |
# File 'lib/rhales/template_engine.rb', line 36 def partial_resolver @partial_resolver end |
#template_content ⇒ Object (readonly)
Returns the value of attribute template_content.
36 37 38 |
# File 'lib/rhales/template_engine.rb', line 36 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
380 381 382 383 384 385 386 387 388 389 390 |
# File 'lib/rhales/template_engine.rb', line 380 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
375 376 377 |
# File 'lib/rhales/template_engine.rb', line 375 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
291 292 293 |
# File 'lib/rhales/template_engine.rb', line 291 def create_each_context(item, index, items_var) EachContext.new(@context, item, index, items_var) end |
#create_merged_context(parent_context, local_data) ⇒ Object (private)
Create a new context with merged data
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/rhales/template_engine.rb', line 301 def create_merged_context(parent_context, local_data) # Extract all props from parent context and merge with local data # Local data takes precedence over parent props merged_props = parent_context.props.merge(local_data) # Create new context with merged props, preserving other context attributes Context.for_view( parent_context.req, parent_context.sess, parent_context.cust, parent_context.locale, config: parent_context.config, **merged_props, ) end |
#data_attributes ⇒ Object
Access all data attributes from parsed .rue file
85 86 87 |
# File 'lib/rhales/template_engine.rb', line 85 def data_attributes @parser&.data_attributes || {} end |
#escape_html(string) ⇒ Object (private)
HTML escape for XSS protection
296 297 298 |
# File 'lib/rhales/template_engine.rb', line 296 def escape_html(string) ERB::Util.html_escape(string) end |
#evaluate_condition(condition) ⇒ Object (private)
Evaluate condition for if/unless blocks
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/rhales/template_engine.rb', line 268 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
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
# File 'lib/rhales/template_engine.rb', line 250 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
95 96 97 |
# File 'lib/rhales/template_engine.rb', line 95 def partials @parser&.partials || [] end |
#render ⇒ Object
45 46 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 |
# File 'lib/rhales/template_engine.rb', line 45 def render # 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 rescue ::Rhales::ParseError => ex # Parse errors already have good error messages with location raise RenderError, "Template parsing failed: #{ex.}" rescue ::Rhales::ValidationError => ex # Validation errors from RueDocument raise RenderError, "Template validation failed: #{ex.}" rescue StandardError => ex 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
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 |
# File 'lib/rhales/template_engine.rb', line 114 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 old format for data sections result += (node) end end result end |
#render_each_block(node) ⇒ Object (private)
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/rhales/template_engine.rb', line 178 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)
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/rhales/template_engine.rb', line 196 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) raw ? value.to_s : escape_html(value.to_s) end end |
#render_if_block(node) ⇒ Object (private)
155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/rhales/template_engine.rb', line 155 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)
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/rhales/template_engine.rb', line 212 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?(/^<(data|template|logic)\b/) # Parse as RueDocument to handle data 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 # Process data section if present and merge with parent context merged_context = @context if partial_doc.section('data') # Create hydrator with parent context to process interpolations hydrator = Hydrator.new(partial_doc, @context) local_data = hydrator.processed_data_hash # Create merged context (local data takes precedence) merged_context = create_merged_context(@context, local_data) end # Render template with merged context engine = self.class.new(template_content, merged_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)
150 151 152 153 |
# File 'lib/rhales/template_engine.rb', line 150 def render_partial_expression(node) partial_name = node.value[:name] render_partial(partial_name) end |
#render_template_string(template_string) ⇒ Object (private)
105 106 107 108 109 110 |
# File 'lib/rhales/template_engine.rb', line 105 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)
167 168 169 170 171 172 173 174 175 176 |
# File 'lib/rhales/template_engine.rb', line 167 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)
142 143 144 145 146 147 148 |
# File 'lib/rhales/template_engine.rb', line 142 def render_variable_expression(node) name = node.value[:name] raw = node.value[:raw] value = get_variable_value(name) raw ? value.to_s : escape_html(value.to_s) end |
#schema_path ⇒ Object
Access schema path from parsed .rue file
80 81 82 |
# File 'lib/rhales/template_engine.rb', line 80 def schema_path @parser&.schema_path end |
#simple_template? ⇒ Boolean (private)
101 102 103 |
# File 'lib/rhales/template_engine.rb', line 101 def simple_template? !@template_content.match?(/^<(data|template|logic)\b/) end |
#template_variables ⇒ Object
Get template variables used in the template
90 91 92 |
# File 'lib/rhales/template_engine.rb', line 90 def template_variables @parser&.template_variables || [] end |
#window_attribute ⇒ Object
Access window attribute from parsed .rue file
75 76 77 |
# File 'lib/rhales/template_engine.rb', line 75 def window_attribute @parser&.window_attribute end |