Class: Rhales::TemplateEngine

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::LoggingHelpers

#format_value, #log_timed_operation, #log_with_metadata

Methods included from Utils

#now, #now_in_μs

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

#contextObject (readonly)

Returns the value of attribute context.



38
39
40
# File 'lib/rhales/core/template_engine.rb', line 38

def context
  @context
end

#parserObject (readonly)

Returns the value of attribute parser.



38
39
40
# File 'lib/rhales/core/template_engine.rb', line 38

def parser
  @parser
end

#partial_resolverObject (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_contentObject (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

#partialsObject

Get all partials used in the template



107
108
109
# File 'lib/rhales/core/template_engine.rb', line 107

def partials
  @parser&.partials || []
end

#renderObject



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.message, line: ex.line, column: ex.column, section: ex.source_type
  )
  raise RenderError, "Template parsing failed: #{ex.message}"
rescue ::Rhales::ValidationError => ex
  # Validation errors from RueDocument
  (Rhales.logger, :error, 'Template validation error',
    error: ex.message, template_type: :rue
  )
  raise RenderError, "Template validation failed: #{ex.message}"
rescue StandardError => ex
  (Rhales.logger, :error, 'Template render error',
    error: ex.message, error_class: ex.class.name
  )
  raise RenderError, "Template rendering failed: #{ex.message}"
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 += render_handlebars_expression(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 render_handlebars_expression(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_pathObject

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)

Returns:

  • (Boolean)


113
114
115
# File 'lib/rhales/core/template_engine.rb', line 113

def simple_template?
  !@template_content.match?(/^<(schema|template|logic)\b/)
end

#template_variablesObject

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_attributeObject

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