Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 36 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,32 @@ Building a Liquid implementation (compiler, interpreter, or transpiler)? liquid-
## How It Works

```
┌─────────────────────────────────────────────────────────────────────────────
liquid-spec
├─────────────────────────────────────────────────────────────────────────────
┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ YAML Spec │ │ Adapter │ │ Your Implementation │ │
│ Files │────▶│ (Bridge) │────▶│ (compile + render) │ │
│ │ │ │ │ │ │
│ • template │ │ LiquidSpec │ │ MyLiquid.parse(src) │ │
│ • env vars │ │ .compile │ │ template.render(vars) │ │
│ • expected │ │ .render │ │ │ │
└─────────────┘ └─────────────┘ └─────────────────────────┘ │
│ │
│ │ │
┌─────────────────────────────────────────────────────────────────
│ Test Runner │
For each spec:
1. Compile template via adapter
2. Render with environment variables
3. Compare output to expected
4. Report pass/fail
└─────────────────────────────────────────────────────────────────
└─────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────────────────────┐
│ liquid-spec │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ YAML Spec │ │ Adapter │ │ Your Implementation │ │
│ │ Files │────▶│ (Bridge) │────▶│ (compile + render) │ │
│ │ │ │ │ │ │ │
│ │ • template │ │ LiquidSpec │ │ MyLiquid.parse(src) │ │
│ │ • env vars │ │ .compile │ │ template.render(vars) │ │
│ │ • expected │ │ .render │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
│ │ │ │
└───────────────────┼────────────────────────┘
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐
│ │ Test Runner │ │
│ │
│ │ For each spec:
│ │ 1. Compile template via adapter
│ │ 2. Render with environment variables
│ │ 3. Compare output to expected
│ │ 4. Report pass/fail
│ └───────────────────────────────────────────────────────────────────┘
│ │
└──────────────────────────────────────────────────────────────────────────┘
```

## Installation
Expand Down Expand Up @@ -84,21 +84,22 @@ end

# Parse template source into a template object
LiquidSpec.compile do |source, options|
# options includes: :line_numbers, :error_mode
MyLiquid::Template.parse(source, **options)
end

# Render a compiled template with test context
LiquidSpec.render do |template, ctx|
template.render(ctx.environment)
# Render a compiled template
LiquidSpec.render do |template, assigns, options|
# assigns = variables hash
# options includes: :registers, :strict_errors, :exception_renderer
template.render(assigns, **options)
end
```

The `ctx` object in render provides:
- `ctx.environment` - Variables to render with (Hash)
- `ctx.file_system` - For `{% include %}` and `{% render %}` tags
- `ctx.error_mode` - `:lax` or `:strict`
- `ctx.registers` - Implementation-specific data
- `ctx.rethrow_errors?` - Whether to raise or capture errors
The `options` hash in render includes:
- `:registers` - Hash with `:file_system` and `:template_factory`
- `:strict_errors` - If true, raise errors; if false, render them inline
- `:exception_renderer` - Custom exception handler (optional)

## Test Suites

Expand Down
17 changes: 8 additions & 9 deletions examples/liquid_c.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,20 @@
end

LiquidSpec.configure do |config|
# liquid-c supports both core and lax parsing
config.features = [:core, :lax_parsing]
end

LiquidSpec.compile do |source, options|
Liquid::Template.parse(source, line_numbers: true, **options)
Liquid::Template.parse(source, **options)
end

LiquidSpec.render do |template, ctx|
liquid_ctx = ctx.context_klass.build(
static_environments: ctx.environment,
registers: Liquid::Registers.new(ctx.registers),
rethrow_errors: ctx.rethrow_errors?,
LiquidSpec.render do |template, assigns, options|
context = Liquid::Context.build(
static_environments: assigns,
registers: Liquid::Registers.new(options[:registers] || {}),
rethrow_errors: options[:strict_errors],
)
liquid_ctx.exception_renderer = ctx.exception_renderer
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]

template.render(liquid_ctx)
template.render(context)
end
24 changes: 10 additions & 14 deletions examples/liquid_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,21 @@
end

LiquidSpec.configure do |config|
# Declare which features this Liquid implementation supports
config.features = [
:core, # Basic Liquid parsing and rendering
:lax_parsing, # Supports error_mode: :lax
]
config.features = [:core, :lax_parsing]
end

LiquidSpec.compile do |source, options|
Liquid::Template.parse(source, line_numbers: true, disable_liquid_c_nodes: true, **options)
Liquid::Template.parse(source, **options)
end

LiquidSpec.render do |template, ctx|
liquid_ctx = ctx.context_klass.build(
static_environments: ctx.environment,
registers: Liquid::Registers.new(ctx.registers),
rethrow_errors: ctx.rethrow_errors?,
LiquidSpec.render do |template, assigns, options|
# Build context with static_environments (read-only assigns that can be shadowed)
context = Liquid::Context.build(
static_environments: assigns,
registers: Liquid::Registers.new(options[:registers] || {}),
rethrow_errors: options[:strict_errors],
)
# Only set exception_renderer if provided (otherwise keep default)
liquid_ctx.exception_renderer = ctx.exception_renderer if ctx.exception_renderer
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]

template.render(liquid_ctx)
template.render(context)
end
17 changes: 8 additions & 9 deletions examples/liquid_ruby_strict.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,21 @@
end

LiquidSpec.configure do |config|
# Only core feature - no lax_parsing support
config.features = [:core]
end

LiquidSpec.compile do |source, options|
# Force strict mode regardless of spec
Liquid::Template.parse(source, line_numbers: true, error_mode: :strict, disable_liquid_c_nodes: true, **options)
Liquid::Template.parse(source, error_mode: :strict, **options)
end

LiquidSpec.render do |template, ctx|
liquid_ctx = ctx.context_klass.build(
static_environments: ctx.environment,
registers: Liquid::Registers.new(ctx.registers),
rethrow_errors: ctx.rethrow_errors?,
LiquidSpec.render do |template, assigns, options|
context = Liquid::Context.build(
static_environments: assigns,
registers: Liquid::Registers.new(options[:registers] || {}),
rethrow_errors: options[:strict_errors],
)
liquid_ctx.exception_renderer = ctx.exception_renderer
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]

template.render(liquid_ctx)
template.render(context)
end
88 changes: 13 additions & 75 deletions lib/liquid/spec/cli/adapter_dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,21 @@ module LiquidSpec
}.freeze

class Configuration
# CLI-controlled options
attr_accessor :suite, :filter, :verbose, :strict_only
attr_reader :features

# Default suite - :all runs all suites the adapter supports
DEFAULT_SUITE = :all
# Adapter-declared features
attr_reader :features

def initialize
@suite = DEFAULT_SUITE
# CLI defaults
@suite = :all
@filter = nil
@verbose = false
@strict_only = false
@features = [:core] # Core is always enabled by default

# Adapter defaults
@features = [:core]
end

# Set the features this adapter implements
Expand All @@ -42,72 +45,6 @@ def features=(list)
def feature?(name)
@features.include?(name.to_sym)
end

# Add features
def add_features(*names)
names.each { |n| @features << n.to_sym unless @features.include?(n.to_sym) }
end
end

# Context passed to render block - provides clean, ready-to-use data
class Context
attr_reader :environment,
:file_system,
:exception_renderer,
:template_factory,
:error_mode,
:render_errors,
:context_klass

def initialize(spec_context)
# Deep copy environment to avoid mutation
# Skip deep copy for recursive structures (they'll cause stack overflow)
env = spec_context[:environment] || {}
@environment = safe_deep_copy(env)

@file_system = spec_context[:file_system]
@exception_renderer = spec_context[:exception_renderer]
@template_factory = spec_context[:template_factory]
@error_mode = spec_context[:error_mode]
@render_errors = spec_context[:render_errors]
@context_klass = spec_context[:context_klass]
end

# Build registers hash with file_system and template_factory
def registers
{
file_system: @file_system,
template_factory: @template_factory,
}.compact
end

# Should errors be rethrown or rendered inline?
def rethrow_errors?
!@render_errors
end

private

def safe_deep_copy(obj, seen = {}.compare_by_identity)
return seen[obj] if seen.key?(obj)

case obj
when Hash
# Preserve the original class (for custom Hash subclasses like HashWithCustomToS)
copy = obj.class.new
seen[obj] = copy
obj.each { |k, v| copy[safe_deep_copy(k, seen)] = safe_deep_copy(v, seen) }
copy
when Array
copy = []
seen[obj] = copy
obj.each { |v| copy << safe_deep_copy(v, seen) }
copy
else
# Immutable or not worth copying (strings, numbers, symbols, drops, etc.)
obj
end
end
end

class << self
Expand All @@ -125,11 +62,13 @@ def setup(&block)
end

# Define how to compile/parse a template
# Block receives: |source, options|
def compile(&block)
@compile_block = block
end

# Define how to render a compiled template
# Block receives: |template, assigns, options|
def render(&block)
@render_block = block
end
Expand Down Expand Up @@ -160,12 +99,11 @@ def do_compile(source, options = {})
end

# Internal: render a template using the adapter
def do_render(template, context)
def do_render(template, assigns, options = {})
run_setup!
raise "No render block defined. Use LiquidSpec.render { |template, ctx| ... }" unless @render_block
raise "No render block defined. Use LiquidSpec.render { |template, assigns, options| ... }" unless @render_block

ctx = Context.new(context)
@render_block.call(template, ctx)
@render_block.call(template, assigns, options)
end

def running_from_cli!
Expand Down
Loading
Loading