Initial github import of liquid

This commit is contained in:
Tobias Lütke
2008-05-08 11:28:13 -04:00
commit 1d647361e1
64 changed files with 5197 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
# LiquidView is a action view extension class. You can register it with rails
# and use liquid as an template system for .liquid files
#
# Example
#
# ActionView::Base::register_template_handler :liquid, LiquidView
class LiquidView
def initialize(action_view)
@action_view = action_view
end
def render(template, local_assigns)
@action_view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
assigns = @action_view.assigns.dup
if content_for_layout = @action_view.instance_variable_get("@content_for_layout")
assigns['content_for_layout'] = content_for_layout
end
assigns.merge!(local_assigns)
liquid = Liquid::Template.parse(template)
liquid.render(assigns, :filters => [@action_view.controller.master_helper_module], :registers => {:action_view => @action_view, :controller => @action_view.controller})
end
end
+68
View File
@@ -0,0 +1,68 @@
# Copyright (c) 2005 Tobias Luetke
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
$LOAD_PATH.unshift(File.dirname(__FILE__))
module Liquid
FilterSperator = /\|/
ArgumentSeparator = ','
FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.'
TagStart = /\{\%/
TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]\??/
VariableStart = /\{\{/
VariableEnd = /\}\}/
VariableIncompleteEnd = /\}\}?/
QuotedFragment = /"[^"]+"|'[^']+'|[^\s,|]+/
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/
TemplateParser = /(#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd})/
VariableParser = /\[[^\]]+\]|#{VariableSegment}+/
end
require 'liquid/drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/strainer'
require 'liquid/context'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/document'
require 'liquid/variable'
require 'liquid/file_system'
require 'liquid/template'
require 'liquid/htmltags'
require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/module_ex'
# Load all the tags of the standard library
#
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
+98
View File
@@ -0,0 +1,98 @@
module Liquid
class Block < Tag
def parse(tokens)
@nodelist ||= []
@nodelist.clear
while token = tokens.shift
case token
when /^#{TagStart}/
if token =~ /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/
# if we found the proper block delimitor just end parsing here and let the outer block
# proceed
if block_delimiter == $1
end_tag
return
end
# fetch the tag from registered blocks
if tag = Template.tags[$1]
@nodelist << tag.new($1, $2, tokens)
else
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag($1, $2, tokens)
end
else
raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} "
end
when /^#{VariableStart}/
@nodelist << create_variable(token)
when ''
# pass
else
@nodelist << token
end
end
# Make sure that its ok to end parsing in the current block.
# Effectively this method will throw and exception unless the current block is
# of type Document
assert_missing_delimitation!
end
def end_tag
end
def unknown_tag(tag, params, tokens)
case tag
when 'else'
raise SyntaxError, "#{block_name} tag does not expect else tag"
when 'end'
raise SyntaxError, "'end' is not a valid delimiter for #{block_name} tags. use #{block_delimiter}"
else
raise SyntaxError, "Unknown tag '#{tag}'"
end
end
def block_delimiter
"end#{block_name}"
end
def block_name
@tag_name
end
def create_variable(token)
token.scan(/^#{VariableStart}(.*)#{VariableEnd}$/) do |content|
return Variable.new(content.first)
end
raise SyntaxError.new("Variable '#{token}' was not properly terminated with regexp: #{VariableEnd.inspect} ")
end
def render(context)
render_all(@nodelist, context)
end
protected
def assert_missing_delimitation!
raise SyntaxError.new("#{block_name} tag was never closed")
end
def render_all(list, context)
list.collect do |token|
begin
token.respond_to?(:render) ? token.render(context) : token
rescue Exception => e
context.handle_error(e)
end
end
end
end
end
+123
View File
@@ -0,0 +1,123 @@
module Liquid
# Container for liquid nodes which conveniently wraps decision making logic
#
# Example:
#
# c = Condition.new('1', '==', '1')
# c.evaluate #=> true
#
class Condition #:nodoc:
@@operators = {
'==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
'!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<' => :<,
'>' => :>,
'>=' => :>=,
'<=' => :<=,
'contains' => lambda { |cond, left, right| left.include?(right) },
}
def self.operators
@@operators
end
attr_reader :attachment
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@left, @operator, @right = left, operator, right
@child_relation = nil
@child_condition = nil
end
def evaluate(context = Context.new)
result = interpret_condition(left, right, operator, context)
case @child_relation
when :or
result || @child_condition.evaluate(context)
when :and
result && @child_condition.evaluate(context)
else
result
end
end
def or(condition)
@child_relation, @child_condition = :or, condition
end
def and(condition)
@child_relation, @child_condition = :and, condition
end
def attach(attachment)
@attachment = attachment
end
def else?
false
end
def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
end
private
def equal_variables(left, right)
if left.is_a?(Symbol)
if right.respond_to?(left)
return right.send(left.to_s)
else
return nil
end
end
if right.is_a?(Symbol)
if left.respond_to?(right)
return left.send(right.to_s)
else
return nil
end
end
left == right
end
def interpret_condition(left, right, op, context)
# If the operator is empty this means that the decision statement is just
# a single variable. We can just poll this variable from the context and
# return this as the result.
return context[left] if op == nil
left, right = context[left], context[right]
operation = self.class.operators[op] || raise(ArgumentError.new("Error in tag '#{name}' - Unknown operator #{op}"))
if operation.respond_to?(:call)
operation.call(self, left, right)
elsif left.respond_to?(operation) and right.respond_to?(operation)
left.send(operation, right)
else
nil
end
end
end
class ElseCondition < Condition
def else?
true
end
def evaluate(context)
true
end
end
end
+245
View File
@@ -0,0 +1,245 @@
module Liquid
class ContextError < StandardError
end
# Context keeps the variable stack and resolves variables, as well as keywords
#
# context['variable'] = 'testing'
# context['variable'] #=> 'testing'
# context['true'] #=> true
# context['10.2232'] #=> 10.2232
#
# context.stack do
# context['bob'] = 'bobsen'
# end
#
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes
attr_reader :errors, :registers
def initialize(assigns = {}, registers = {}, rethrow_errors = false)
@scopes = [(assigns || {})]
@registers = registers
@errors = []
@rethrow_errors = rethrow_errors
end
def strainer
@strainer ||= Strainer.create(self)
end
# adds filters to this context.
# this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
# for that
def add_filters(filters)
filters = [filters].flatten.compact
filters.each do |f|
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
strainer.extend(f)
end
end
def handle_error(e)
errors.push(e)
raise if @rethrow_errors
case e
when SyntaxError then "Liquid syntax error: #{e.message}"
else "Liquid error: #{e.message}"
end
end
def invoke(method, *args)
if strainer.respond_to?(method)
strainer.__send__(method, *args)
else
args.first
end
end
# push new local scope on the stack. use <tt>Context#stack</tt> instead
def push
@scopes.unshift({})
end
# merge a hash of variables in the current local scope
def merge(new_scopes)
@scopes[0].merge!(new_scopes)
end
# pop from the stack. use <tt>Context#stack</tt> instead
def pop
raise ContextError if @scopes.size == 1
@scopes.shift
end
# pushes a new local scope on the stack, pops it at the end of the block
#
# Example:
#
# context.stack do
# context['var'] = 'hi'
# end
# context['var] #=> nil
#
def stack(&block)
result = nil
push
begin
result = yield
ensure
pop
end
result
end
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value)
@scopes[0][key] = value
end
def [](key)
resolve(key)
end
def has_key?(key)
resolve(key) != nil
end
private
# Look up variable, either resolve directly after considering the name. We can directly handle
# Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
#
# Example:
#
# products == empty #=> products.empty?
#
def resolve(key)
case key
when nil, 'nil', 'null', ''
nil
when 'true'
true
when 'false'
false
when 'blank'
:blank?
when 'empty'
:empty?
# Single quoted strings
when /^'(.*)'$/
$1.to_s
# Double quoted strings
when /^"(.*)"$/
$1.to_s
# Integer and floats
when /^(\d+)$/
$1.to_i
# Ranges
when /^\((\S+)\.\.(\S+)\)$/
(resolve($1).to_i..resolve($2).to_i)
# Floats
when /^(\d[\d\.]+)$/
$1.to_f
else
variable(key)
end
end
# fetches an object starting at the local scope and then moving up
# the hierachy
def find_variable(key)
@scopes.each do |scope|
if scope.has_key?(key)
variable = scope[key]
variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
end
end
nil
end
# resolves namespaced queries gracefully.
#
# Example
#
# @context['hash'] = {"name" => 'tobi'}
# assert_equal 'tobi', @context['hash.name']
# assert_equal 'tobi', @context['hash[name]']
#
def variable(markup)
parts = markup.scan(VariableParser)
square_bracketed = /^\[(.*)\]$/
first_part = parts.shift
if first_part =~ square_bracketed
first_part = resolve($1)
end
if object = find_variable(first_part)
parts.each do |part|
# If object is a hash we look for the presence of the key and if its available
# we return it
if part =~ square_bracketed
part = resolve($1)
object[pos] = object[part].call(self) if object[part].is_a?(Proc) and object.respond_to?(:[]=)
object = object[part].to_liquid
else
# Hash
if object.respond_to?(:has_key?) and object.has_key?(part)
# if its a proc we will replace the entry in the hash table with the proc
res = object[part]
res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=)
object = res.to_liquid
# Array
elsif object.respond_to?(:fetch) and part =~ /^\d+$/
pos = part.to_i
object[pos] = object[pos].call(self) if object[pos].is_a?(Proc) and object.respond_to?(:[]=)
object = object[pos].to_liquid
# Some special cases. If no key with the same name was found we interpret following calls
# as commands and call them on the current object
elsif object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
object = object.send(part.intern).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
else
return nil
end
end
# If we are dealing with a drop here we have to
object.context = self if object.respond_to?(:context=)
end
end
object
end
private
def execute_proc(proc)
proc.call(self)
end
end
end
+17
View File
@@ -0,0 +1,17 @@
module Liquid
class Document < Block
# we don't need markup to open this block
def initialize(tokens)
parse(tokens)
end
# There isn't a real delimter
def block_delimiter
[]
end
# Document blocks don't need to be terminated since they are not actually opened
def assert_missing_delimitation!
end
end
end
+48
View File
@@ -0,0 +1,48 @@
module Liquid
# A drop in liquid is a class which allows you to to export DOM like things to liquid
# Methods of drops are callable.
# The main use for liquid drops is the implement lazy loaded objects.
# If you would like to make data available to the web designers which you don't want loaded unless needed then
# a drop is a great way to do that
#
# Example:
#
# class ProductDrop < Liquid::Drop
# def top_sales
# Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
# end
# end
#
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
#
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
# catch all
class Drop
attr_writer :context
# Catch all for the method
def before_method(method)
nil
end
# called by liquid to invoke a drop
def invoke_drop(method)
result = before_method(method)
result ||= send(method.to_sym) if self.class.public_instance_methods.include?(method.to_s)
result
end
def has_key?(name)
true
end
def to_liquid
self
end
alias :[] :invoke_drop
end
end
+7
View File
@@ -0,0 +1,7 @@
module Liquid
class FilterNotFound < StandardError
end
class FileSystemError < StandardError
end
end
+56
View File
@@ -0,0 +1,56 @@
require 'time'
require 'date'
class String # :nodoc:
def to_liquid
self
end
end
class Array # :nodoc:
def to_liquid
self
end
end
class Hash # :nodoc:
def to_liquid
self
end
end
class Numeric # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
def to_liquid
self
end
end
class DateTime < Date # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
def to_liquid
self
end
end
def true.to_liquid # :nodoc:
self
end
def false.to_liquid # :nodoc:
self
end
def nil.to_liquid # :nodoc:
self
end
+62
View File
@@ -0,0 +1,62 @@
module Liquid
# A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
#
# You can implement subclasses that retrieve templates from the database, from the file system using a different
# path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
#
# You can add additional instance variables, arguments, or methods as needed.
#
# Example:
#
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template)
#
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
class BlankFileSystem
# Called by Liquid to retrieve a template file
def read_template_file(template_path)
raise FileSystemError, "This liquid context does not allow includes."
end
end
# This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
# ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
#
# For security reasons, template paths are only allowed to contain letters, numbers, and underscore.
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path")
#
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
#
class LocalFileSystem
attr_accessor :root
def initialize(root)
@root = root
end
def read_template_file(template_path)
full_path = full_path(template_path)
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
File.read(full_path)
end
def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
full_path = if template_path.include?('/')
File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
else
File.join(root, "_#{template_path}.liquid")
end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
full_path
end
end
end
+74
View File
@@ -0,0 +1,74 @@
module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
end
super
end
def render(context)
collection = context[@collection_name] or return ''
if @attributes['limit'] or @attributes['offset']
limit = context[@attributes['limit']] || -1
offset = context[@attributes['offset']] || 0
collection = collection[offset.to_i..(limit.to_i + offset.to_i - 1)]
end
length = collection.length
cols = context[@attributes['cols']].to_i
row = 1
col = 0
result = ["<tr class=\"row1\">\n"]
context.stack do
collection.each_with_index do |item, index|
context[@variable_name] = item
context['tablerowloop'] = {
'length' => length,
'index' => index + 1,
'index0' => index,
'col' => col + 1,
'col0' => col,
'index0' => index,
'rindex' => length - index,
'rindex0' => length - index -1,
'first' => (index == 0),
'last' => (index == length - 1),
'col_first' => (col == 0),
'col_last' => (col == cols - 1)
}
col += 1
result << ["<td class=\"col#{col}\">"] + render_all(@nodelist, context) + ['</td>']
if col == cols and not (index == length - 1)
col = 0
row += 1
result << ["</tr>\n<tr class=\"row#{row}\">"]
end
end
end
result + ["</tr>\n"]
end
end
Template.register_tag('tablerow', TableRow)
end
+62
View File
@@ -0,0 +1,62 @@
# Copyright 2007 by Domizio Demichelis
# This library is free software. It may be used, redistributed and/or modified
# under the same terms as Ruby itself
#
# This extension is usesd in order to expose the object of the implementing class
# to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
# to the allowed method passed with the liquid_methods call
# Example:
#
# class SomeClass
# liquid_methods :an_allowed_method
#
# def an_allowed_method
# 'this comes from an allowed method'
# end
# def unallowed_method
# 'this will never be an output'
# end
# end
#
# if you want to extend the drop to other methods you can defines more methods
# in the class <YourClass>::LiquidDropClass
#
# class SomeClass::LiquidDropClass
# def another_allowed_method
# 'and this from another allowed method'
# end
# end
# end
#
# usage:
# @something = SomeClass.new
#
# template:
# {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
#
# output:
# 'this comes from an allowed method and this from another allowed method'
#
# You can also chain associations, by adding the liquid_method call in the
# association models.
#
class Module
def liquid_methods(*allowed_methods)
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
define_method :to_liquid do
drop_class.new(self)
end
drop_class.class_eval do
def initialize(object)
@object = object
end
allowed_methods.each do |sym|
define_method sym do
@object.send sym
end
end
end
end
end
+164
View File
@@ -0,0 +1,164 @@
require 'cgi'
module Liquid
module StandardFilters
# Return the size of an array or of an string
def size(input)
input.respond_to?(:size) ? input.size : 0
end
# convert a input string to DOWNCASE
def downcase(input)
input.to_s.downcase
end
# convert a input string to UPCASE
def upcase(input)
input.to_s.upcase
end
# capitalize words in the input centence
def capitalize(input)
input.to_s.capitalize
end
def escape(input)
CGI.escapeHTML(input) rescue input
end
alias_method :h, :escape
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...")
if input.nil? then return end
l = length.to_i - truncate_string.length
l = 0 if l < 0
input.length > length.to_i ? input[0...l] + truncate_string : input
end
def truncatewords(input, words = 15, truncate_string = "...")
if input.nil? then return end
wordlist = input.to_s.split
l = words.to_i - 1
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
end
def strip_html(input)
input.to_s.gsub(/<.*?>/, '')
end
# Remove all newlines from the string
def strip_newlines(input)
input.to_s.gsub(/\n/, '')
end
# Join elements of the array with certain character between them
def join(input, glue = ' ')
[input].flatten.join(glue)
end
# Sort elements of the array
def sort(input)
[input].flatten.sort
end
# Replace occurrences of a string with another
def replace(input, string, replacement = '')
input.to_s.gsub(string, replacement)
end
# Replace the first occurrences of a string with another
def replace_first(input, string, replacement = '')
input.to_s.sub(string, replacement)
end
# remove a substring
def remove(input, string)
input.to_s.gsub(string, '')
end
# remove the first occurrences of a substring
def remove_first(input, string)
input.to_s.sub(string, '')
end
# Add <br /> tags in front of all newlines in input string
def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n")
end
# Reformat a date
#
# %a - The abbreviated weekday name (``Sun'')
# %A - The full weekday name (``Sunday'')
# %b - The abbreviated month name (``Jan'')
# %B - The full month name (``January'')
# %c - The preferred local date and time representation
# %d - Day of the month (01..31)
# %H - Hour of the day, 24-hour clock (00..23)
# %I - Hour of the day, 12-hour clock (01..12)
# %j - Day of the year (001..366)
# %m - Month of the year (01..12)
# %M - Minute of the hour (00..59)
# %p - Meridian indicator (``AM'' or ``PM'')
# %S - Second of the minute (00..60)
# %U - Week number of the current year,
# starting with the first Sunday as the first
# day of the first week (00..53)
# %W - Week number of the current year,
# starting with the first Monday as the first
# day of the first week (00..53)
# %w - Day of the week (Sunday is 0, 0..6)
# %x - Preferred representation for the date alone, no time
# %X - Preferred representation for the time alone, no date
# %y - Year without a century (00..99)
# %Y - Year with century
# %Z - Time zone name
# %% - Literal ``%'' character
def date(input, format)
if format.to_s.empty?
return input.to_s
end
date = case input
when String
Time.parse(input)
when Date, Time, DateTime
input
else
return input
end
date.strftime(format.to_s)
rescue => e
input
end
# Get the first element of the passed in array
#
# Example:
# {{ product.images | first | to_img }}
#
def first(array)
array.first if array.respond_to?(:first)
end
# Get the last element of the passed in array
#
# Example:
# {{ product.images | last | to_img }}
#
def last(array)
array.last if array.respond_to?(:last)
end
end
Template.register_filter(StandardFilters)
end
+52
View File
@@ -0,0 +1,52 @@
require 'set'
module Liquid
parent_object = if defined? BlankObject
BlankObject
else
Object
end
# Strainer is the parent class for the filters system.
# New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
#
# One of the strainer's responsibilities is to keep malicious method calls out
class Strainer < parent_object #:nodoc:
INTERNAL_METHOD = /^__/
@@required_methods = Set.new([:__send__, :__id__, :respond_to?, :extend, :methods, :class])
@@filters = {}
def initialize(context)
@context = context
end
def self.global_filter(filter)
raise StandardError, "Passed filter is not a module" unless filter.is_a?(Module)
@@filters[filter.name] = filter
end
def self.create(context)
strainer = Strainer.new(context)
@@filters.each { |k,m| strainer.extend(m) }
strainer
end
def respond_to?(method)
method_name = method.to_s
return false if method_name =~ INTERNAL_METHOD
return false if @@required_methods.include?(method_name)
super
end
# remove all standard methods from the bucket so circumvent security
# problems
instance_methods.each do |m|
unless @@required_methods.include?(m.to_sym)
undef_method m
end
end
end
end
+26
View File
@@ -0,0 +1,26 @@
module Liquid
class Tag
attr_accessor :nodelist
def initialize(tag_name, markup, tokens)
@tag_name = tag_name
@markup = markup
parse(tokens)
end
def parse(tokens)
end
def name
self.class.name.downcase
end
def render(context)
''
end
end
end
+33
View File
@@ -0,0 +1,33 @@
module Liquid
# Assign sets a variable in your template.
#
# {% assign foo = 'monkey' %}
#
# You can then use the variable later in the page.
#
# {{ monkey }}
#
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@to = $1
@from = $2
else
raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
end
super
end
def render(context)
context.scopes.last[@to.to_s] = context[@from]
''
end
end
Template.register_tag('assign', Assign)
end
+35
View File
@@ -0,0 +1,35 @@
module Liquid
# Capture stores the result of a block into a variable without rendering it inplace.
#
# {% capture heading %}
# Monkeys!
# {% endcapture %}
# ...
# <h1>{{ monkeys }}</h1>
#
# Capture is useful for saving content for use later in your template, such as
# in a sidebar or footer.
#
class Capture < Block
Syntax = /(\w+)/
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@to = $1
else
raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
end
super
end
def render(context)
output = super
context[@to] = output.to_s
''
end
end
Template.register_tag('capture', Capture)
end
+83
View File
@@ -0,0 +1,83 @@
module Liquid
class Case < Block
Syntax = /(#{QuotedFragment})/
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/
def initialize(tag_name, markup, tokens)
@blocks = []
if markup =~ Syntax
@left = $1
else
raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
end
super
end
def unknown_tag(tag, markup, tokens)
@nodelist = []
case tag
when 'when'
record_when_condition(markup)
when 'else'
record_else_condition(markup)
else
super
end
end
def render(context)
context.stack do
execute_else_block = true
@blocks.inject([]) do |output, block|
if block.else?
return render_all(block.attachment, context) if execute_else_block
elsif block.evaluate(context)
execute_else_block = false
output += render_all(block.attachment, context)
end
output
end
end
end
private
def record_when_condition(markup)
while markup
# Create a new nodelist and assign it to the new block
if not markup =~ WhenSyntax
raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ")
end
markup = $2
block = Condition.new(@left, '==', $1)
block.attach(@nodelist)
@blocks.push(block)
end
end
def record_else_condition(markup)
if not markup.strip.empty?
raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ")
end
block = ElseCondition.new
block.attach(@nodelist)
@blocks << block
end
end
Template.register_tag('case', Case)
end
+9
View File
@@ -0,0 +1,9 @@
module Liquid
class Comment < Block
def render(context)
''
end
end
Template.register_tag('comment', Comment)
end
+60
View File
@@ -0,0 +1,60 @@
module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
#
# {% for item in items %}
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
# {% end %}
#
# <div class="red"> Item one </div>
# <div class="green"> Item two </div>
# <div class="blue"> Item three </div>
# <div class="red"> Item four </div>
# <div class="green"> Item five</div>
#
class Cycle < Tag
SimpleSyntax = /#{QuotedFragment}/
NamedSyntax = /(#{QuotedFragment})\s*\:\s*(.*)/
def initialize(tag_name, markup, tokens)
case markup
when NamedSyntax
@variables = variables_from_string($2)
@name = $1
when SimpleSyntax
@variables = variables_from_string(markup)
@name = "'#{@variables.to_s}'"
else
raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]")
end
super
end
def render(context)
context.registers[:cycle] ||= Hash.new(0)
context.stack do
key = context[@name]
iteration = context.registers[:cycle][key]
result = context[@variables[iteration]]
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
result
end
end
private
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/
$1 ? $1 : nil
end.compact
end
end
Template.register_tag('cycle', Cycle)
end
+118
View File
@@ -0,0 +1,118 @@
module Liquid
# "For" iterates over an array or collection.
# Several useful variables are available to you within the loop.
#
# == Basic usage:
# {% for item in collection %}
# {{ forloop.index }}: {{ item.name }}
# {% endfor %}
#
# == Advanced usage:
# {% for item in collection %}
# <div {% if forloop.first %}class="first"{% endif %}>
# Item {{ forloop.index }}: {{ item.name }}
# </div>
# {% endfor %}
#
# You can also define a limit and offset much like SQL. Remember
# that offset starts at 0 for the first item.
#
# {% for item in collection limit:5 offset:10 %}
# {{ item.name }}
# {% end %}
#
# == Available variables:
#
# forloop.name:: 'item-collection'
# forloop.length:: Length of the loop
# forloop.index:: The current item's position in the collection;
# forloop.index starts at 1.
# This is helpful for non-programmers who start believe
# the first item in an array is 1, not 0.
# forloop.index0:: The current item's position in the collection
# where the first item is 0
# forloop.rindex:: Number of items remaining in the loop
# (length - index) where 1 is the last item.
# forloop.rindex0:: Number of items remaining in the loop
# where 0 is the last item.
# forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item.
#
class For < Block
Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@name = "#{$1}-#{$2}"
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
end
super
end
def render(context)
context.registers[:for] ||= Hash.new(0)
collection = context[@collection_name]
collection = collection.to_a if collection.is_a?(Range)
return '' if collection.nil? or collection.empty?
range = (0..collection.length)
if @attributes['limit'] or @attributes['offset']
offset = 0
if @attributes['offset'] == 'continue'
offset = context.registers[:for][@name]
else
offset = context[@attributes['offset']] || 0
end
limit = context[@attributes['limit']]
range_end = limit ? offset + limit : collection.length
range = (offset..range_end-1)
# Save the range end in the registers so that future calls to
# offset:continue have something to pick up
context.registers[:for][@name] = range_end
end
result = []
segment = collection[range]
return '' if segment.nil?
context.stack do
length = segment.length
segment.each_with_index do |item, index|
context[@variable_name] = item
context['forloop'] = {
'name' => @name,
'length' => length,
'index' => index + 1,
'index0' => index,
'rindex' => length - index,
'rindex0' => length - index -1,
'first' => (index == 0),
'last' => (index == length - 1) }
result << render_all(@nodelist, context)
end
end
# Store position of last element we rendered. This allows us to do
result
end
end
Template.register_tag('for', For)
end
+79
View File
@@ -0,0 +1,79 @@
module Liquid
# If is the conditional block
#
# {% if user.admin %}
# Admin user!
# {% else %}
# Not admin user
# {% endif %}
#
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
#
class If < Block
SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]"
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/
def initialize(tag_name, markup, tokens)
@blocks = []
push_block('if', markup)
super
end
def unknown_tag(tag, markup, tokens)
if ['elsif', 'else'].include?(tag)
push_block(tag, markup)
else
super
end
end
def render(context)
context.stack do
@blocks.each do |block|
if block.evaluate(context)
return render_all(block.attachment, context)
end
end
''
end
end
private
def push_block(tag, markup)
block = if tag == 'else'
ElseCondition.new
else
expressions = markup.split(/\b(and|or)\b/).reverse
raise SyntaxHelp unless expressions.shift =~ Syntax
condition = Condition.new($1, $2, $3)
while not expressions.empty?
operator = expressions.shift
raise SyntaxHelp unless expressions.shift.to_s =~ Syntax
new_condition = Condition.new($1, $2, $3)
new_condition.send(operator.to_sym, condition)
condition = new_condition
end
condition
end
@blocks.push(block)
@nodelist = block.attach(Array.new)
end
end
Template.register_tag('if', If)
end
+20
View File
@@ -0,0 +1,20 @@
module Liquid
class Ifchanged < Block
def render(context)
context.stack do
output = render_all(@nodelist, context)
if output != context.registers[:ifchanged]
context.registers[:ifchanged] = output
output
else
''
end
end
end
end
Template.register_tag('ifchanged', Ifchanged)
end
+55
View File
@@ -0,0 +1,55 @@
module Liquid
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@template_name = $1
@variable_name = $3
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]")
end
super
end
def parse(tokens)
end
def render(context)
source = Liquid::Template.file_system.read_template_file(context[@template_name])
partial = Liquid::Template.parse(source)
variable = context[@variable_name || @template_name[1..-2]]
context.stack do
@attributes.each do |key, value|
context[key] = context[value]
end
if variable.is_a?(Array)
variable.collect do |variable|
context[@template_name[1..-2]] = variable
partial.render(context)
end
else
context[@template_name[1..-2]] = variable
partial.render(context)
end
end
end
end
Template.register_tag('include', Include)
end
+33
View File
@@ -0,0 +1,33 @@
require File.dirname(__FILE__) + '/if'
module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic.
#
# {% unless x < 0 %} x is greater than zero {% end %}
#
class Unless < If
def render(context)
context.stack do
# First condition is interpreted backwards ( if not )
block = @blocks.first
unless block.evaluate(context)
return render_all(block.attachment, context)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return render_all(block.attachment, context)
end
end
''
end
end
end
Template.register_tag('unless', Unless)
end
+145
View File
@@ -0,0 +1,145 @@
module Liquid
# Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the
# source code you got. During compile time some extensive error checking is performed.
# your code should expect to get some SyntaxErrors.
#
# After you have a compiled template you can then <tt>render</tt> it.
# You can use a compiled template over and over again and keep it cached.
#
# Example:
#
# template = Liquid::Template.parse(source)
# template.render('user_name' => 'bob')
#
class Template
attr_accessor :root
@@file_system = BlankFileSystem.new
class <<self
def file_system
@@file_system
end
def file_system=(obj)
@@file_system = obj
end
def register_tag(name, klass)
tags[name.to_s] = klass
end
def tags
@tags ||= {}
end
# Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library
def register_filter(mod)
Strainer.global_filter(mod)
end
# creates a new <tt>Template</tt> object from liquid source code
def parse(source)
template = Template.new
template.parse(source)
template
end
end
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
def initialize
end
# Parse source code.
# Returns self for easy chaining
def parse(source)
@root = Document.new(tokenize(source))
self
end
def registers
@registers ||= {}
end
def assigns
@assigns ||= {}
end
def errors
@errors ||= []
end
# Render takes a hash with local variables.
#
# if you use the same filters over and over again consider registering them globally
# with <tt>Template.register_filter</tt>
#
# Following options can be passed:
#
# * <tt>filters</tt> : array with local filters
# * <tt>registers</tt> : hash with register variables. Those can be accessed from
# filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
return '' if @root.nil?
context = case args.first
when Liquid::Context
args.shift
when Hash
self.assigns.merge!(args.shift)
Context.new(assigns, registers, @rethrow_errors)
when nil
Context.new(assigns, registers, @rethrow_errors)
else
raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
end
case args.last
when Hash
options = args.pop
if options[:registers].is_a?(Hash)
self.registers.merge!(options[:registers])
end
if options[:filters]
context.add_filters(options[:filters])
end
when Module
context.add_filters(args.pop)
when Array
context.add_filters(args.pop)
end
# render the nodelist.
# for performance reasons we get a array back here. to_s will make a string out of it
begin
@root.render(context).join
ensure
@errors = context.errors
end
end
def render!(*args)
@rethrow_errors = true; render(*args)
end
private
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
def tokenize(source)
return [] if source.to_s.empty?
tokens = source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
tokens
end
end
end
+52
View File
@@ -0,0 +1,52 @@
module Liquid
# Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage
#
# {{ monkey }}
# {{ user.name }}
#
# Variables can be combined with filters:
#
# {{ user | link }}
#
class Variable
attr_accessor :filters, :name
def initialize(markup)
@markup = markup
@name = nil
@filters = []
if match = markup.match(/\s*(#{QuotedFragment})/)
@name = match[1]
if markup.match(/#{FilterSperator}\s*(.*)/)
filters = Regexp.last_match(1).split(/#{FilterSperator}/)
filters.each do |f|
if matches = f.match(/\s*(\w+)/)
filtername = matches[1]
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten
@filters << [filtername.to_sym, filterargs]
end
end
end
end
end
def render(context)
return '' if @name.nil?
output = context[@name]
@filters.inject(output) do |output, filter|
filterargs = filter[1].to_a.collect do |a|
context[a]
end
begin
output = context.invoke(filter[0], output, *filterargs)
rescue FilterNotFound
raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
end
end
output
end
end
end