mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-17 09:50:48 -07:00
This adds a new `compile_to_ruby` method to Liquid::Template that compiles
Liquid templates to pure Ruby code. The compiled code can be eval'd to create
a proc that renders templates without needing the Liquid library at runtime.
## Features
- Compiles all standard Liquid tags: if/unless/case, for, assign, capture,
cycle, increment/decrement, raw, echo, break/continue, comment, tablerow
- Compiles variable expressions with filter chains to direct Ruby method calls
- Supports static partial inlining ({% render %} and {% include %} with string
literals are loaded and compiled at compile time)
- Dynamic partial support via runtime callbacks (__render_dynamic__, __include_dynamic__)
- Debug mode with source comments for error tracing (lightweight source map)
- SourceMapper utility to trace runtime errors back to Liquid source
## Optimization Opportunities
The compiled Ruby code has significant performance advantages:
1. No Context object - variables accessed directly from assigns hash
2. No filter invocation overhead - direct Ruby method calls
3. No resource limits tracking - no per-node render score updates
4. No stack-based scoping - uses Ruby's native block scoping
5. Direct string concatenation - no render_to_output_buffer abstraction
6. Native control flow - break/continue use Ruby's throw/catch
7. No to_liquid calls - values used directly
8. No profiling hooks - no profiler overhead
9. No exception rendering - errors propagate naturally
## Usage
```ruby
template = Liquid::Template.parse("Hello, {{ name }}!")
ruby_code = template.compile_to_ruby
render_proc = eval(ruby_code)
result = render_proc.call({ "name" => "World" })
# => "Hello, World!"
# With debug mode for error tracing:
ruby_code = template.compile_to_ruby(debug: true)
```
## Limitations
- Dynamic {% render %} and {% include %} require runtime callback methods
- Custom tags need explicit compiler implementations
- Custom filters must be available at runtime
126 lines
4.1 KiB
Ruby
126 lines
4.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Liquid
|
|
module Compile
|
|
# ConditionCompiler compiles Liquid conditions to Ruby boolean expressions.
|
|
#
|
|
# Handles:
|
|
# - Simple truthiness: {% if variable %}
|
|
# - Comparisons: {% if a == b %}, {% if a > b %}
|
|
# - Logical operators: {% if a and b %}, {% if a or b %}
|
|
# - Special checks: {% if a == blank %}, {% if a == empty %}
|
|
class ConditionCompiler
|
|
# Operator mappings from Liquid to Ruby
|
|
OPERATORS = {
|
|
'==' => '==',
|
|
'!=' => '!=',
|
|
'<>' => '!=',
|
|
'<' => '<',
|
|
'>' => '>',
|
|
'<=' => '<=',
|
|
'>=' => '>=',
|
|
'contains' => :contains,
|
|
}.freeze
|
|
|
|
# Compile a Condition to a Ruby boolean expression
|
|
# @param condition [Liquid::Condition] The condition
|
|
# @param compiler [RubyCompiler] The main compiler instance
|
|
# @return [String] Ruby code expression that evaluates to true/false
|
|
def self.compile(condition, compiler)
|
|
if condition.is_a?(ElseCondition)
|
|
return "true"
|
|
end
|
|
|
|
compile_condition_chain(condition, compiler)
|
|
end
|
|
|
|
private
|
|
|
|
def self.compile_condition_chain(condition, compiler)
|
|
# Compile the current condition
|
|
current = compile_single_condition(condition, compiler)
|
|
|
|
# Check for chained conditions (and/or)
|
|
if condition.child_condition
|
|
child = compile_condition_chain(condition.child_condition, compiler)
|
|
child_relation = condition.send(:child_relation)
|
|
|
|
case child_relation
|
|
when :and
|
|
"(#{current} && #{child})"
|
|
when :or
|
|
"(#{current} || #{child})"
|
|
else
|
|
current
|
|
end
|
|
else
|
|
current
|
|
end
|
|
end
|
|
|
|
def self.compile_single_condition(condition, compiler)
|
|
left = condition.left
|
|
op = condition.operator
|
|
right = condition.right
|
|
|
|
# If no operator, just check truthiness
|
|
if op.nil?
|
|
left_expr = ExpressionCompiler.compile(left, compiler)
|
|
return "__truthy__(#{left_expr})"
|
|
end
|
|
|
|
# Compile left and right expressions
|
|
left_expr = compile_condition_value(left, compiler)
|
|
right_expr = compile_condition_value(right, compiler)
|
|
|
|
# Handle special operators
|
|
case OPERATORS[op]
|
|
when :contains
|
|
compile_contains(left_expr, right_expr, compiler)
|
|
when '=='
|
|
compile_equality(left, right, left_expr, right_expr, compiler)
|
|
when '!='
|
|
"!(#{compile_equality(left, right, left_expr, right_expr, compiler)})"
|
|
else
|
|
# Standard comparison
|
|
ruby_op = OPERATORS[op] || op
|
|
"(#{left_expr} #{ruby_op} #{right_expr} rescue false)"
|
|
end
|
|
end
|
|
|
|
def self.compile_condition_value(expr, compiler)
|
|
if expr.is_a?(Condition::MethodLiteral)
|
|
# For blank/empty checks, we return a special marker
|
|
# The equality handler will deal with this
|
|
":__method_literal_#{expr.method_name}__"
|
|
else
|
|
ExpressionCompiler.compile(expr, compiler)
|
|
end
|
|
end
|
|
|
|
def self.compile_equality(left, right, left_expr, right_expr, compiler)
|
|
# Handle blank/empty method literals
|
|
if left.is_a?(Condition::MethodLiteral)
|
|
method_name = left.method_name
|
|
"(#{right_expr}.respond_to?(:#{method_name}) ? #{right_expr}.#{method_name} : nil)"
|
|
elsif right.is_a?(Condition::MethodLiteral)
|
|
method_name = right.method_name
|
|
"(#{left_expr}.respond_to?(:#{method_name}) ? #{left_expr}.#{method_name} : nil)"
|
|
else
|
|
"(#{left_expr} == #{right_expr})"
|
|
end
|
|
end
|
|
|
|
def self.compile_contains(left_expr, right_expr, compiler)
|
|
# The contains operator checks if left includes right
|
|
# For strings, right is converted to a string
|
|
"(lambda { |left, right| " \
|
|
"return false if left.nil? || right.nil? || !left.respond_to?(:include?); " \
|
|
"right = right.to_s if left.is_a?(String); " \
|
|
"left.include?(right) rescue false " \
|
|
"}.call(#{left_expr}, #{right_expr}))"
|
|
end
|
|
end
|
|
end
|
|
end
|