mirror of
https://github.com/kemko/liquid.git
synced 2026-01-02 08:15:41 +03:00
Compare commits
80 Commits
v2.3.0
...
liquid_c_e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb807e9484 | ||
|
|
e53884ce08 | ||
|
|
78fe69259d | ||
|
|
afe8474a2b | ||
|
|
e404f6d8e1 | ||
|
|
8867d6a65f | ||
|
|
141fa898d0 | ||
|
|
58b04bae74 | ||
|
|
28737171f7 | ||
|
|
594b6da8c5 | ||
|
|
cf5fa5923d | ||
|
|
7cc10b9abf | ||
|
|
20beec2de3 | ||
|
|
06486fd21d | ||
|
|
7dcf578898 | ||
|
|
006e7475f4 | ||
|
|
f7d1e1d0c1 | ||
|
|
28fd2222c8 | ||
|
|
9913895b81 | ||
|
|
d706db3bd7 | ||
|
|
38b4543bf1 | ||
|
|
1300210f05 | ||
|
|
a48e162237 | ||
|
|
7bcb565668 | ||
|
|
c3e6cde67f | ||
|
|
50bd34fd78 | ||
|
|
ee41b3f4a3 | ||
|
|
05d9976e16 | ||
|
|
6c2fde5eea | ||
|
|
ce76dbf8d9 | ||
|
|
661ff2ccdf | ||
|
|
9c183bea83 | ||
|
|
484fd18612 | ||
|
|
bf86459456 | ||
|
|
d2827c561b | ||
|
|
16c34595a4 | ||
|
|
6e091909ee | ||
|
|
d7cb39ccb3 | ||
|
|
f8d46804fd | ||
|
|
5c6de2d919 | ||
|
|
a8e9327f0b | ||
|
|
f5a20ff8e8 | ||
|
|
d0184555d9 | ||
|
|
6ebdded8f2 | ||
|
|
515b31158e | ||
|
|
40cc799f3d | ||
|
|
5ac91e0837 | ||
|
|
f6cb54fa59 | ||
|
|
1606b4b705 | ||
|
|
7cfd0f15d1 | ||
|
|
25ba54fc52 | ||
|
|
1aff63ff57 | ||
|
|
08fdcbbf65 | ||
|
|
2dba9ed0ea | ||
|
|
6d02d59fbd | ||
|
|
281e3ea9c8 | ||
|
|
b51b30fac1 | ||
|
|
84ed3d9964 | ||
|
|
c10f936d2a | ||
|
|
1ee342d83b | ||
|
|
0e3b522fe2 | ||
|
|
db07e2b67e | ||
|
|
b8d7b9aeda | ||
|
|
3d7c1c80a0 | ||
|
|
1b2d0198ea | ||
|
|
043d816460 | ||
|
|
974ea40cca | ||
|
|
d8b416187a | ||
|
|
58ad90677b | ||
|
|
2b04590d4b | ||
|
|
bce0033c65 | ||
|
|
01dea94671 | ||
|
|
1a1b4702d7 | ||
|
|
85d1dc0d07 | ||
|
|
0eafe7f2fd | ||
|
|
815e4e2b8d | ||
|
|
ef98715b12 | ||
|
|
c4d713b6bb | ||
|
|
975b17b529 | ||
|
|
745d875e79 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,3 +4,8 @@
|
||||
pkg
|
||||
*.rbc
|
||||
.rvmrc
|
||||
*.o
|
||||
*.bundle
|
||||
ext/liquid/Makefile
|
||||
ext/liquid/liquid_context.c
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
rvm:
|
||||
- 1.8.7
|
||||
- 1.9.2
|
||||
- 1.9.3
|
||||
- ree
|
||||
- jruby
|
||||
- rbx
|
||||
- rbx-2.0
|
||||
- jruby-18mode
|
||||
- jruby-19mode
|
||||
- rbx-18mode
|
||||
- rbx-19mode
|
||||
|
||||
script: "rake test"
|
||||
|
||||
|
||||
21
History.md
21
History.md
@@ -1,6 +1,25 @@
|
||||
# Liquid Version History
|
||||
|
||||
## 2.3.0
|
||||
## 2.5.0 / 2013-03-06
|
||||
|
||||
* Prevent Object methods from being called on drops
|
||||
* Avoid symbol injection from liquid
|
||||
* Added break and continue statements
|
||||
* Fix filter parser for args without space separators
|
||||
* Add support for filter keyword arguments
|
||||
|
||||
## 2.4.0 / 2012-08-03
|
||||
|
||||
* Performance improvements
|
||||
* Allow filters in `assign`
|
||||
* Add `modulo` filter
|
||||
* Ruby 1.8, 1.9, and Rubinius compatibility fixes
|
||||
* Add support for `quoted['references']` in `tablerow`
|
||||
* Add support for Enumerable to `tablerow`
|
||||
* `strip_html` filter removes html comments
|
||||
|
||||
|
||||
## 2.3.0 / 2011-10-16
|
||||
|
||||
* Several speed/memory improvements
|
||||
* Numerous bug fixes
|
||||
|
||||
12
README.md
12
README.md
@@ -6,15 +6,15 @@ Liquid is a template engine which was written with very specific requirements:
|
||||
|
||||
* It has to have beautiful and simple markup. Template engines which don't produce good looking markup are no fun to use.
|
||||
* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote.
|
||||
* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects.
|
||||
* It has to be stateless. Compile and render steps have to be separate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects.
|
||||
|
||||
## Why you should use Liquid
|
||||
|
||||
* You want to allow your users to edit the appearance of your application but don't want them to run **insecure code on your server**.
|
||||
* You want to render templates directly from the database
|
||||
* You like smarty (PHP) style template engines
|
||||
* You need a template engine which does HTML just as well as emails
|
||||
* You don't like the markup of your current templating engine
|
||||
* You want to render templates directly from the database.
|
||||
* You like smarty (PHP) style template engines.
|
||||
* You need a template engine which does HTML just as well as emails.
|
||||
* You don't like the markup of your current templating engine.
|
||||
|
||||
## What does it look like?
|
||||
|
||||
@@ -31,7 +31,7 @@ Liquid is a template engine which was written with very specific requirements:
|
||||
</ul>
|
||||
```
|
||||
|
||||
## Howto use Liquid
|
||||
## How to use Liquid
|
||||
|
||||
Liquid supports a very simple API based around the Liquid::Template class.
|
||||
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
||||
|
||||
50
Rakefile
50
Rakefile
@@ -2,14 +2,52 @@
|
||||
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/clean'
|
||||
require 'fileutils'
|
||||
require 'rake/testtask'
|
||||
require 'rubygems/package_task'
|
||||
|
||||
task :default => 'test'
|
||||
task :default => [:compile, :test]
|
||||
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
task :gen do
|
||||
sh "leg -oext/liquid/liquid_context.c ext/liquid/liquid_context.leg"
|
||||
end
|
||||
|
||||
task :compile => [:gen, :liquid_ext]
|
||||
|
||||
extension = "liquid_ext"
|
||||
ext = "ext/liquid"
|
||||
ext_so = "#{ext}/#{extension}.#{RbConfig::CONFIG['DLEXT']}"
|
||||
ext_files = FileList[
|
||||
"#{ext}/*.c",
|
||||
"#{ext}/*.h",
|
||||
"#{ext}/*.leg",
|
||||
"#{ext}/extconf.rb",
|
||||
"#{ext}/Makefile",
|
||||
"lib"
|
||||
]
|
||||
|
||||
task "lib" do
|
||||
directory "lib"
|
||||
end
|
||||
|
||||
desc "Builds just the #{extension} extension"
|
||||
task extension.to_sym => [:gen, "#{ext}/Makefile", ext_so ]
|
||||
|
||||
file "#{ext}/Makefile" => ["#{ext}/extconf.rb"] do
|
||||
Dir.chdir(ext) do ruby "extconf.rb" end
|
||||
end
|
||||
|
||||
file ext_so => ext_files do
|
||||
Dir.chdir(ext) do
|
||||
sh "make"
|
||||
end
|
||||
cp ext_so, "lib"
|
||||
end
|
||||
|
||||
Rake::TestTask.new(:test => [:gen, 'liquid_ext']) do |t|
|
||||
t.libs << '.' << 'lib' << 'test'
|
||||
t.pattern = 'test/liquid/**/*_test.rb'
|
||||
t.test_files = FileList['test/liquid/**/*_test.rb']
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
@@ -27,7 +65,7 @@ namespace :benchmark do
|
||||
|
||||
desc "Run the liquid benchmark"
|
||||
task :run do
|
||||
ruby "performance/benchmark.rb"
|
||||
ruby "./performance/benchmark.rb"
|
||||
end
|
||||
|
||||
end
|
||||
@@ -37,12 +75,12 @@ namespace :profile do
|
||||
|
||||
desc "Run the liquid profile/performance coverage"
|
||||
task :run do
|
||||
ruby "performance/profile.rb"
|
||||
ruby "./performance/profile.rb"
|
||||
end
|
||||
|
||||
desc "Run KCacheGrind"
|
||||
task :grind => :run do
|
||||
system "kcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
|
||||
system "qcachegrind /tmp//callgrind.liquid.txt"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
6
ext/liquid/extconf.rb
Normal file
6
ext/liquid/extconf.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
require 'mkmf'
|
||||
|
||||
dir_config("liquid_ext")
|
||||
have_library("c", "main")
|
||||
|
||||
create_makefile("liquid_ext")
|
||||
98
ext/liquid/liquid_context.leg
Normal file
98
ext/liquid/liquid_context.leg
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
%{
|
||||
//uncomment to get more debug instrumentation
|
||||
//
|
||||
//#define YY_DEBUG
|
||||
|
||||
#include <ruby.h>
|
||||
|
||||
#define EMIT(sym, data) \
|
||||
rb_ary_push(ctx->rb_tokens, rb_ary_new3(2, ID2SYM(rb_intern(sym)), data));
|
||||
|
||||
#define yy_rb_str rb_str_new(yytext, yyleng)
|
||||
|
||||
#define YYSTYPE VALUE
|
||||
#define YY_CTX_LOCAL
|
||||
#define YY_CTX_MEMBERS VALUE rb_tokens; char *p; int p_len;
|
||||
|
||||
#define YY_INPUT(buf, result, max_size) { \
|
||||
result = ctx->p_len; \
|
||||
if (result>0 || EOF == ctx->p[0]) { \
|
||||
if (max_size < result) { result = max_size; } \
|
||||
strncpy(buf, ctx->p, result); \
|
||||
buf[result] = '\0'; \
|
||||
yyprintf((stderr, "\nREFILLING %d chars now:<%s>", result, buf)); \
|
||||
ctx->p += result; ctx->p_len -= result; \
|
||||
yyprintf((stderr, "\nREFILLING DONE size left: %d <%s>", ctx->p_len, ctx->p)); \
|
||||
} \
|
||||
}
|
||||
%}
|
||||
|
||||
grammar = primary
|
||||
| entity
|
||||
| range
|
||||
| hash { EMIT("lookup", Qnil); }
|
||||
;
|
||||
|
||||
hash = '[' (primary|entity) ']';
|
||||
|
||||
primary = var:const { EMIT("id", var); }
|
||||
| var:string { EMIT("id", var); }
|
||||
| var:numeric { EMIT("id", var); }
|
||||
;
|
||||
|
||||
accessors = hash { EMIT("call", Qnil); }
|
||||
| '.first' { EMIT("buildin", rb_str_new2("first")); }
|
||||
| '.last' { EMIT("buildin", rb_str_new2("last")); }
|
||||
| '.size' { EMIT("buildin", rb_str_new2("size")); }
|
||||
| '.' <identifier> { EMIT("id", yy_rb_str); EMIT("call", Qnil); }
|
||||
;
|
||||
|
||||
entity = <identifier> { EMIT("id", yy_rb_str); EMIT("lookup", Qnil); }
|
||||
accessors*
|
||||
;
|
||||
|
||||
rangelet = var:integer { EMIT("id", var); }
|
||||
| entity
|
||||
;
|
||||
|
||||
range = '(' rangelet '..' rangelet ')' { EMIT("range", Qnil); }
|
||||
|
||||
|
||||
string = ['] < ( !['] . )* > ['] { $$ = yy_rb_str; }
|
||||
| ["] < ( !["] . )* > ["] { $$ = yy_rb_str; }
|
||||
;
|
||||
|
||||
numeric = float
|
||||
| integer
|
||||
;
|
||||
|
||||
float = <'-'? digit+'.'digit+> { $$ = rb_funcall(rb_cObject, rb_intern("Float"), 1, yy_rb_str); }
|
||||
integer = <'-'? digit+> { $$ = rb_funcall(rb_cObject, rb_intern("Integer"), 1, yy_rb_str); }
|
||||
|
||||
|
||||
const = "true" { $$ = Qtrue; }
|
||||
| 'false' { $$ = Qfalse; }
|
||||
| 'nil' { $$ = Qnil; }
|
||||
| 'null' { $$ = Qnil; }
|
||||
;
|
||||
|
||||
digit = [0-9];
|
||||
identifier = [a-zA-Z][a-zA-Z0-9_\-]*[?!]?;
|
||||
|
||||
%%
|
||||
|
||||
VALUE liquid_context_parse_impl(VALUE self, VALUE text) {
|
||||
char *p;
|
||||
int len;
|
||||
yycontext ctx;
|
||||
|
||||
memset(&ctx, 0, sizeof(yycontext));
|
||||
ctx.p = RSTRING_PTR(text);
|
||||
ctx.p_len = (int) RSTRING_LEN(text);
|
||||
ctx.rb_tokens = rb_ary_new();
|
||||
|
||||
yyparse(&ctx);
|
||||
|
||||
return ctx.rb_tokens;
|
||||
}
|
||||
13
ext/liquid/liquid_ext.c
Normal file
13
ext/liquid/liquid_ext.c
Normal file
@@ -0,0 +1,13 @@
|
||||
#include <ruby.h>
|
||||
|
||||
static VALUE rb_Liquid;
|
||||
static VALUE rb_Parser;
|
||||
|
||||
VALUE liquid_context_parse_impl(VALUE text);
|
||||
|
||||
void Init_liquid_ext()
|
||||
{
|
||||
rb_Liquid = rb_define_module("Liquid");
|
||||
rb_Parser = rb_define_class_under(rb_Liquid, "Parser", rb_cObject);
|
||||
rb_define_singleton_method(rb_Parser, "parse", liquid_context_parse_impl, 1);
|
||||
}
|
||||
@@ -32,22 +32,23 @@ module Liquid
|
||||
VariableEnd = /\}\}/
|
||||
VariableIncompleteEnd = /\}\}?/
|
||||
QuotedString = /"[^"]*"|'[^']*'/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/
|
||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s|:,]+/
|
||||
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/
|
||||
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/
|
||||
SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/
|
||||
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/
|
||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/
|
||||
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/o
|
||||
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/o
|
||||
SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
|
||||
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/o
|
||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||
AnyStartingTag = /\{\{|\{\%/
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/
|
||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/o
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/o
|
||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
||||
end
|
||||
|
||||
require 'liquid/drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer'
|
||||
require 'liquid/context'
|
||||
require 'liquid/tag'
|
||||
@@ -60,6 +61,8 @@ require 'liquid/htmltags'
|
||||
require 'liquid/standardfilters'
|
||||
require 'liquid/condition'
|
||||
require 'liquid/module_ex'
|
||||
require 'liquid/utils'
|
||||
require 'liquid_ext'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module Liquid
|
||||
|
||||
class Block < Tag
|
||||
IsTag = /^#{TagStart}/
|
||||
IsVariable = /^#{VariableStart}/
|
||||
FullToken = /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/
|
||||
ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/
|
||||
IsTag = /^#{TagStart}/o
|
||||
IsVariable = /^#{VariableStart}/o
|
||||
FullToken = /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
|
||||
ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/o
|
||||
|
||||
def parse(tokens)
|
||||
@nodelist ||= []
|
||||
@@ -89,13 +89,27 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_all(list, context)
|
||||
list.collect do |token|
|
||||
output = []
|
||||
list.each do |token|
|
||||
# Break out if we have any unhanded interrupts.
|
||||
break if context.has_interrupt?
|
||||
|
||||
begin
|
||||
token.respond_to?(:render) ? token.render(context) : token
|
||||
# If we get an Interrupt that means the block must stop processing. An
|
||||
# Interrupt is any command that stops block execution such as {% break %}
|
||||
# or {% continue %}
|
||||
if token.is_a? Continue or token.is_a? Break
|
||||
context.push_interrupt(token.interrupt)
|
||||
break
|
||||
end
|
||||
|
||||
output << (token.respond_to?(:render) ? token.render(context) : token)
|
||||
rescue ::StandardError => e
|
||||
context.handle_error(e)
|
||||
output << (context.handle_error(e))
|
||||
end
|
||||
end.join
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module Liquid
|
||||
|
||||
|
||||
# Context keeps the variable stack and resolves variables, as well as keywords
|
||||
#
|
||||
# context['variable'] = 'testing'
|
||||
@@ -22,6 +23,8 @@ module Liquid
|
||||
@errors = []
|
||||
@rethrow_errors = rethrow_errors
|
||||
squash_instance_assigns_with_environments
|
||||
|
||||
@interrupts = []
|
||||
end
|
||||
|
||||
def strainer
|
||||
@@ -37,10 +40,26 @@ module Liquid
|
||||
|
||||
filters.each do |f|
|
||||
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
|
||||
Strainer.add_known_filter(f)
|
||||
strainer.extend(f)
|
||||
end
|
||||
end
|
||||
|
||||
# are there any not handled interrupts?
|
||||
def has_interrupt?
|
||||
!@interrupts.empty?
|
||||
end
|
||||
|
||||
# push an interrupt to the stack. this interrupt is considered not handled.
|
||||
def push_interrupt(e)
|
||||
@interrupts.push(e)
|
||||
end
|
||||
|
||||
# pop an interrupt from the stack
|
||||
def pop_interrupt
|
||||
@interrupts.pop
|
||||
end
|
||||
|
||||
def handle_error(e)
|
||||
errors.push(e)
|
||||
raise if @rethrow_errors
|
||||
@@ -54,11 +73,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def invoke(method, *args)
|
||||
if strainer.respond_to?(method)
|
||||
strainer.__send__(method, *args)
|
||||
else
|
||||
args.first
|
||||
end
|
||||
strainer.invoke(method, *args)
|
||||
end
|
||||
|
||||
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
||||
@@ -97,54 +112,89 @@ module Liquid
|
||||
@scopes[0] = {}
|
||||
end
|
||||
|
||||
|
||||
# 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, ""
|
||||
return nil
|
||||
when "blank"
|
||||
return :blank?
|
||||
when "empty"
|
||||
return :empty?
|
||||
end
|
||||
|
||||
result = Parser.parse(key)
|
||||
stack = []
|
||||
|
||||
result.each do |(sym, value)|
|
||||
|
||||
case sym
|
||||
when :id
|
||||
stack.push value
|
||||
when :lookup
|
||||
left = stack.pop
|
||||
value = find_variable(left)
|
||||
|
||||
stack.push(harden(value))
|
||||
when :range
|
||||
right = stack.pop.to_i
|
||||
left = stack.pop.to_i
|
||||
|
||||
stack.push (left..right)
|
||||
when :buildin
|
||||
left = stack.pop
|
||||
value = invoke_buildin(left, value)
|
||||
|
||||
stack.push(harden(value))
|
||||
when :call
|
||||
left = stack.pop
|
||||
right = stack.pop
|
||||
value = lookup_and_evaluate(right, left)
|
||||
|
||||
stack.push(harden(value))
|
||||
else
|
||||
raise "unknown #{sym}"
|
||||
end
|
||||
end
|
||||
|
||||
return stack.first
|
||||
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
|
||||
LITERALS = {
|
||||
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => :blank?,
|
||||
'empty' => :empty?
|
||||
}
|
||||
alias_method :[], :resolve
|
||||
|
||||
# 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)
|
||||
if LITERALS.key?(key)
|
||||
LITERALS[key]
|
||||
private
|
||||
|
||||
def invoke_buildin(obj, key)
|
||||
# as weird as this is, liquid unit tests demand that we prioritize hash lookups
|
||||
# to buildins. So if we got a hash and it has a :first element we need to call that
|
||||
# instead of sending the first message...
|
||||
|
||||
if obj.respond_to?(:has_key?) && obj.has_key?(key)
|
||||
return lookup_and_evaluate(obj, key)
|
||||
end
|
||||
|
||||
if obj.respond_to?(key)
|
||||
return obj.send(key)
|
||||
else
|
||||
case key
|
||||
when /^'(.*)'$/ # Single quoted strings
|
||||
$1
|
||||
when /^"(.*)"$/ # Double quoted strings
|
||||
$1
|
||||
when /^(\d+)$/ # Integer and floats
|
||||
$1.to_i
|
||||
when /^\((\S+)\.\.(\S+)\)$/ # Ranges
|
||||
(resolve($1).to_i..resolve($2).to_i)
|
||||
when /^(\d[\d\.]+)$/ # Floats
|
||||
$1.to_f
|
||||
else
|
||||
variable(key)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -164,71 +214,34 @@ module Liquid
|
||||
scope ||= @environments.last || @scopes.last
|
||||
variable ||= lookup_and_evaluate(scope, key)
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
return variable
|
||||
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|
|
||||
part = resolve($1) if part_resolved = (part =~ square_bracketed)
|
||||
|
||||
# If object is a hash- or array-like object we look for the
|
||||
# presence of the key and if its available we return it
|
||||
if object.respond_to?(:[]) and
|
||||
((object.respond_to?(:has_key?) and object.has_key?(part)) or
|
||||
(object.respond_to?(:fetch) and part.is_a?(Integer)))
|
||||
|
||||
# if its a proc we will replace the entry with the proc
|
||||
res = lookup_and_evaluate(object, part)
|
||||
object = res.to_liquid
|
||||
|
||||
# Some special cases. If the part wasn't in square brackets and
|
||||
# no key with the same name was found we interpret following calls
|
||||
# as commands and call them on the current object
|
||||
elsif !part_resolved and 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
|
||||
|
||||
# If we are dealing with a drop here we have to
|
||||
object.context = self if object.respond_to?(:context=)
|
||||
end
|
||||
end
|
||||
|
||||
object
|
||||
end # variable
|
||||
|
||||
def lookup_and_evaluate(obj, key)
|
||||
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
||||
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
||||
else
|
||||
value
|
||||
return nil unless obj.respond_to?(:[])
|
||||
|
||||
if obj.is_a?(Array)
|
||||
return nil unless key.is_a?(Integer)
|
||||
end
|
||||
end # lookup_and_evaluate
|
||||
|
||||
value = obj[key]
|
||||
|
||||
if value.is_a?(Proc)
|
||||
# call the proc
|
||||
value = (value.arity == 0) ? value.call : value.call(self)
|
||||
|
||||
# memozie if possible
|
||||
obj[key] = value if obj.respond_to?(:[]=)
|
||||
end
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
def harden(value)
|
||||
value = value.to_liquid
|
||||
value.context = self if value.respond_to?(:context=)
|
||||
return value
|
||||
end
|
||||
|
||||
def squash_instance_assigns_with_environments
|
||||
@scopes.last.each_key do |k|
|
||||
@@ -240,6 +253,7 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end # squash_instance_assigns_with_environments
|
||||
|
||||
end # Context
|
||||
|
||||
end # Liquid
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
|
||||
# A drop in liquid is a class which allows you to to export DOM like things to liquid.
|
||||
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
||||
# Methods of drops are callable.
|
||||
# The main use for liquid drops is the implement lazy loaded objects.
|
||||
# The main use for liquid drops is to 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
|
||||
# a drop is a great way to do that.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
@@ -18,10 +20,12 @@ module Liquid
|
||||
# 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
|
||||
# catch all.
|
||||
class Drop
|
||||
attr_writer :context
|
||||
|
||||
EMPTY_STRING = ''.freeze
|
||||
|
||||
# Catch all for the method
|
||||
def before_method(method)
|
||||
nil
|
||||
@@ -29,8 +33,8 @@ module Liquid
|
||||
|
||||
# called by liquid to invoke a drop
|
||||
def invoke_drop(method_or_key)
|
||||
if method_or_key && method_or_key != '' && self.class.public_method_defined?(method_or_key.to_s.to_sym)
|
||||
send(method_or_key.to_s.to_sym)
|
||||
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
|
||||
send(method_or_key)
|
||||
else
|
||||
before_method(method_or_key)
|
||||
end
|
||||
@@ -45,5 +49,13 @@ module Liquid
|
||||
end
|
||||
|
||||
alias :[] :invoke_drop
|
||||
|
||||
private
|
||||
|
||||
# Check for method existence without invoking respond_to?, which creates symbols
|
||||
def self.invokable?(method_name)
|
||||
@invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
|
||||
@invokable_methods.include?(method_name.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,4 +8,4 @@ module Liquid
|
||||
class StandardError < Error; end
|
||||
class SyntaxError < Error; end
|
||||
class StackLevelError < Error; end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
module Liquid
|
||||
class TableRow < Block
|
||||
Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
|
||||
def initialize(tag_name, markup, tokens)
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
@collection_name = $2
|
||||
@idx_i = "#{$1}-#{$2}-i"
|
||||
@idx_col = "#{$1}-#{$2}-c"
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = value
|
||||
@@ -18,13 +20,14 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
context.registers[:tablerowloop] ||= Hash.new(0)
|
||||
|
||||
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
|
||||
from = @attributes['offset'] ? context[@attributes['offset']].to_i : 0
|
||||
to = @attributes['limit'] ? from + context[@attributes['limit']].to_i : nil
|
||||
|
||||
collection = Utils.slice_collection_using_each(collection, from, to)
|
||||
|
||||
length = collection.length
|
||||
|
||||
@@ -33,26 +36,15 @@ module Liquid
|
||||
row = 1
|
||||
col = 0
|
||||
|
||||
result = "<tr class=\"row1\">\n"
|
||||
result = ["<tr class=\"row1\">\n"]
|
||||
context.stack do
|
||||
|
||||
context.registers[:tablerowloop][@idx]
|
||||
context['tablerowloop'] = lambda { Tablerowloop.new(@idx_i, @idx_col, length) }
|
||||
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)
|
||||
}
|
||||
|
||||
context.registers[:tablerowloop][@idx_i] = index
|
||||
context.registers[:tablerowloop][@idx_col] = col
|
||||
context[@variable_name] = item
|
||||
|
||||
col += 1
|
||||
|
||||
@@ -69,6 +61,58 @@ module Liquid
|
||||
result << "</tr>\n"
|
||||
result
|
||||
end
|
||||
|
||||
|
||||
|
||||
private
|
||||
|
||||
class Tablerowloop < Liquid::Drop
|
||||
attr_accessor :length
|
||||
|
||||
def initialize(idx_i, idx_col, length)
|
||||
@idx_i, @idx_col, @length = idx_i, idx_col, length
|
||||
end
|
||||
|
||||
def index
|
||||
@context.registers[:tablerowloop][@idx_i] + 1
|
||||
end
|
||||
|
||||
def index0
|
||||
@context.registers[:tablerowloop][@idx_i]
|
||||
end
|
||||
|
||||
def rindex
|
||||
length - @context.registers[:tablerowloop][@idx_i]
|
||||
end
|
||||
|
||||
def rindex0
|
||||
length - @context.registers[:tablerowloop][@idx_i] - 1
|
||||
end
|
||||
|
||||
def first
|
||||
(@context.registers[:tablerowloop][@idx_i] == 0)
|
||||
end
|
||||
|
||||
def last
|
||||
(@context.registers[:tablerowloop][@idx_i] == length - 1)
|
||||
end
|
||||
|
||||
def col
|
||||
@context.registers[:tablerowloop][@idx_col] + 1
|
||||
end
|
||||
|
||||
def col0
|
||||
@context.registers[:tablerowloop][@idx_col]
|
||||
end
|
||||
|
||||
def col_first
|
||||
(@context.registers[:tablerowloop][@idx_col] == 0)
|
||||
end
|
||||
|
||||
def col_last
|
||||
(@context.registers[:tablerowloop][@idx_col] == cols - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('tablerow', TableRow)
|
||||
|
||||
17
lib/liquid/interrupts.rb
Normal file
17
lib/liquid/interrupts.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module Liquid
|
||||
|
||||
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
||||
class Interrupt
|
||||
attr_reader :message
|
||||
|
||||
def initialize(message=nil)
|
||||
@message = message || "interrupt"
|
||||
end
|
||||
end
|
||||
|
||||
# Interrupt that is thrown whenever a {% break %} is called.
|
||||
class BreakInterrupt < Interrupt; end
|
||||
|
||||
# Interrupt that is thrown whenever a {% continue %} is called.
|
||||
class ContinueInterrupt < Interrupt; end
|
||||
end
|
||||
@@ -54,12 +54,16 @@ module Liquid
|
||||
end
|
||||
|
||||
# Split input string into an array of substrings separated by given pattern.
|
||||
#
|
||||
# Example:
|
||||
# <div class="summary">{{ post | split '//' | first }}</div>
|
||||
#
|
||||
def split(input, pattern)
|
||||
input.split(pattern)
|
||||
end
|
||||
|
||||
def strip_html(input)
|
||||
input.to_s.gsub(/<script.*?<\/script>/, '').gsub(/<.*?>/, '')
|
||||
input.to_s.gsub(/<script.*?<\/script>/, '').gsub(/<!--.*?-->/, '').gsub(/<.*?>/, '')
|
||||
end
|
||||
|
||||
# Remove all newlines from the string
|
||||
@@ -218,6 +222,10 @@ module Liquid
|
||||
to_number(input) / to_number(operand)
|
||||
end
|
||||
|
||||
def modulo(input, operand)
|
||||
to_number(input) % to_number(operand)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_number(obj)
|
||||
|
||||
@@ -2,24 +2,15 @@ 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.
|
||||
# New filters are mixed into the strainer class which is then instantiated 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([:__id__, :__send__, :respond_to?, :kind_of?, :extend, :methods, :singleton_methods, :class, :object_id])
|
||||
|
||||
# Ruby 1.9.2 introduces Object#respond_to_missing?, which is invoked by Object#respond_to?
|
||||
@@required_methods << :respond_to_missing? if Object.respond_to? :respond_to_missing?
|
||||
|
||||
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
||||
# Context#add_filters or Template.register_filter
|
||||
class Strainer #:nodoc:
|
||||
@@filters = {}
|
||||
@@known_filters = Set.new
|
||||
@@known_methods = Set.new
|
||||
|
||||
def initialize(context)
|
||||
@context = context
|
||||
@@ -27,28 +18,36 @@ module Liquid
|
||||
|
||||
def self.global_filter(filter)
|
||||
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
|
||||
add_known_filter(filter)
|
||||
@@filters[filter.name] = filter
|
||||
end
|
||||
|
||||
def self.add_known_filter(filter)
|
||||
unless @@known_filters.include?(filter)
|
||||
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
|
||||
new_methods = filter.instance_methods.map(&:to_s)
|
||||
new_methods.reject!{ |m| @@method_blacklist.include?(m) }
|
||||
@@known_methods.merge(new_methods)
|
||||
@@known_filters.add(filter)
|
||||
end
|
||||
end
|
||||
|
||||
def self.create(context)
|
||||
strainer = Strainer.new(context)
|
||||
@@filters.each { |k,m| strainer.extend(m) }
|
||||
strainer
|
||||
end
|
||||
|
||||
def respond_to?(method, include_private = false)
|
||||
method_name = method.to_s
|
||||
return false if method_name =~ INTERNAL_METHOD
|
||||
return false if @@required_methods.include?(method_name)
|
||||
super
|
||||
def invoke(method, *args)
|
||||
if invokable?(method)
|
||||
send(method, *args)
|
||||
else
|
||||
args.first
|
||||
end
|
||||
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
|
||||
def invokable?(method)
|
||||
@@known_methods.include?(method.to_s) && respond_to?(method)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,12 +9,12 @@ module Liquid
|
||||
# {{ foo }}
|
||||
#
|
||||
class Assign < Tag
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o
|
||||
|
||||
def initialize(tag_name, markup, tokens)
|
||||
if markup =~ Syntax
|
||||
@to = $1
|
||||
@from = $2
|
||||
@from = Variable.new($2)
|
||||
else
|
||||
raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
|
||||
end
|
||||
@@ -23,7 +23,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
context.scopes.last[@to] = context[@from]
|
||||
context.scopes.last[@to] = @from.render(context)
|
||||
''
|
||||
end
|
||||
|
||||
|
||||
21
lib/liquid/tags/break.rb
Normal file
21
lib/liquid/tags/break.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module Liquid
|
||||
|
||||
# Break tag to be used to break out of a for loop.
|
||||
#
|
||||
# == Basic Usage:
|
||||
# {% for item in collection %}
|
||||
# {% if item.condition %}
|
||||
# {% break %}
|
||||
# {% endif %}
|
||||
# {% endfor %}
|
||||
#
|
||||
class Break < Tag
|
||||
|
||||
def interrupt
|
||||
BreakInterrupt.new
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Template.register_tag('break', Break)
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
module Liquid
|
||||
class Case < Block
|
||||
Syntax = /(#{QuotedFragment})/
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/o
|
||||
|
||||
def initialize(tag_name, markup, tokens)
|
||||
@blocks = []
|
||||
@@ -31,7 +31,7 @@ module Liquid
|
||||
context.stack do
|
||||
execute_else_block = true
|
||||
|
||||
output = ''
|
||||
output = []
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
return render_all(block.attachment, context) if execute_else_block
|
||||
|
||||
21
lib/liquid/tags/continue.rb
Normal file
21
lib/liquid/tags/continue.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module Liquid
|
||||
|
||||
# Continue tag to be used to break out of a for loop.
|
||||
#
|
||||
# == Basic Usage:
|
||||
# {% for item in collection %}
|
||||
# {% if item.condition %}
|
||||
# {% continue %}
|
||||
# {% endif %}
|
||||
# {% endfor %}
|
||||
#
|
||||
class Continue < Tag
|
||||
|
||||
def interrupt
|
||||
ContinueInterrupt.new
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Template.register_tag('continue', Continue)
|
||||
end
|
||||
@@ -13,8 +13,8 @@ module Liquid
|
||||
# <div class="green"> Item five</div>
|
||||
#
|
||||
class Cycle < Tag
|
||||
SimpleSyntax = /^#{QuotedFragment}+/
|
||||
NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/
|
||||
SimpleSyntax = /^#{QuotedFragment}+/o
|
||||
NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/o
|
||||
|
||||
def initialize(tag_name, markup, tokens)
|
||||
case markup
|
||||
@@ -48,7 +48,7 @@ module Liquid
|
||||
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
$1 ? $1 : nil
|
||||
end.compact
|
||||
end
|
||||
@@ -56,4 +56,4 @@ module Liquid
|
||||
end
|
||||
|
||||
Template.register_tag('cycle', Cycle)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,13 +44,14 @@ module Liquid
|
||||
# forloop.last:: Returns true if the item is the last item.
|
||||
#
|
||||
class For < Block
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
|
||||
def initialize(tag_name, markup, tokens)
|
||||
if markup =~ Syntax
|
||||
@variable_name = $1
|
||||
@collection_name = $2
|
||||
@name = "#{$1}-#{$2}"
|
||||
@name = "#{$1}-#{$2}"
|
||||
@idx = "#{@name}-i"
|
||||
@reversed = $3
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@@ -69,13 +70,14 @@ module Liquid
|
||||
@nodelist = @else_block = []
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render(context)
|
||||
context.registers[:for] ||= Hash.new(0)
|
||||
|
||||
collection = context[@collection_name]
|
||||
collection = collection.to_a if collection.is_a?(Range)
|
||||
|
||||
return render_else(context) unless collection.respond_to?(:each)
|
||||
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||
return render_else(context) unless iterable?(collection)
|
||||
|
||||
from = if @attributes['offset'] == 'continue'
|
||||
context.registers[:for][@name].to_i
|
||||
@@ -85,66 +87,81 @@ module Liquid
|
||||
|
||||
limit = context[@attributes['limit']]
|
||||
to = limit ? limit.to_i + from : nil
|
||||
|
||||
|
||||
segment = slice_collection_using_each(collection, from, to)
|
||||
|
||||
|
||||
segment = Utils.slice_collection_using_each(collection, from, to)
|
||||
|
||||
return render_else(context) if segment.empty?
|
||||
|
||||
segment.reverse! if @reversed
|
||||
|
||||
result = ''
|
||||
result = []
|
||||
|
||||
length = segment.length
|
||||
|
||||
# Store our progress through the collection for the continue flag
|
||||
context.registers[:for][@name] = from + segment.length
|
||||
|
||||
context.stack do
|
||||
segment.each_with_index do |item, index|
|
||||
context.stack do
|
||||
context['forloop'] = lambda { Forloop.new(@name, @idx, length) }
|
||||
segment.each_with_index do |item, index|
|
||||
context.registers[:for][@idx] = 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(@for_block, context)
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
if context.has_interrupt?
|
||||
interrupt = context.pop_interrupt
|
||||
break if interrupt.is_a? BreakInterrupt
|
||||
next if interrupt.is_a? ContinueInterrupt
|
||||
end
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
yielded = 0
|
||||
collection.each do |item|
|
||||
|
||||
if to && to <= index
|
||||
break
|
||||
end
|
||||
|
||||
if from <= index
|
||||
segments << item
|
||||
end
|
||||
|
||||
index += 1
|
||||
end
|
||||
|
||||
segments
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
class Forloop < Liquid::Drop
|
||||
attr_accessor :name, :length
|
||||
|
||||
def initialize(name, idx, length)
|
||||
@name, @idx, @length = name, idx, length
|
||||
end
|
||||
|
||||
def index
|
||||
@context.registers[:for][@idx] + 1
|
||||
end
|
||||
|
||||
def index0
|
||||
@context.registers[:for][@idx]
|
||||
end
|
||||
|
||||
def rindex
|
||||
length - @context.registers[:for][@idx]
|
||||
end
|
||||
|
||||
def rindex0
|
||||
length - @context.registers[:for][@idx] - 1
|
||||
end
|
||||
|
||||
def first
|
||||
(@context.registers[:for][@idx] == 0)
|
||||
end
|
||||
|
||||
def last
|
||||
(@context.registers[:for][@idx] == length - 1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def render_else(context)
|
||||
return @else_block ? [render_all(@else_block, context)] : ''
|
||||
end
|
||||
|
||||
def iterable?(collection)
|
||||
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('for', For)
|
||||
|
||||
@@ -13,8 +13,8 @@ module Liquid
|
||||
#
|
||||
class If < Block
|
||||
SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/
|
||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/
|
||||
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
|
||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
||||
|
||||
def initialize(tag_name, markup, tokens)
|
||||
@blocks = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module Liquid
|
||||
class Include < Tag
|
||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/
|
||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
||||
|
||||
def initialize(tag_name, markup, tokens)
|
||||
if markup =~ Syntax
|
||||
@@ -62,4 +62,4 @@ module Liquid
|
||||
end
|
||||
|
||||
Template.register_tag('include', Include)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -121,8 +121,7 @@ module Liquid
|
||||
begin
|
||||
# render the nodelist.
|
||||
# for performance reasons we get a array back here. join will make a string out of it
|
||||
result = @root.render(context)
|
||||
result.respond_to?(:join) ? result.join : result
|
||||
@root.render(context).join
|
||||
ensure
|
||||
@errors = context.errors
|
||||
end
|
||||
|
||||
31
lib/liquid/utils.rb
Normal file
31
lib/liquid/utils.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module Liquid
|
||||
module Utils
|
||||
def self.slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
yielded = 0
|
||||
|
||||
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||
return [collection] if non_blank_string?(collection)
|
||||
|
||||
collection.each do |item|
|
||||
|
||||
if to && to <= index
|
||||
break
|
||||
end
|
||||
|
||||
if from <= index
|
||||
segments << item
|
||||
end
|
||||
|
||||
index += 1
|
||||
end
|
||||
|
||||
segments
|
||||
end
|
||||
|
||||
def self.non_blank_string?(collection)
|
||||
collection.is_a?(String) && collection != ''
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -11,22 +11,22 @@ module Liquid
|
||||
# {{ user | link }}
|
||||
#
|
||||
class Variable
|
||||
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?!(?:#{FilterSeparator}))(?:#{QuotedFragment}|\S+)\s*)+)/
|
||||
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
|
||||
attr_accessor :filters, :name
|
||||
|
||||
def initialize(markup)
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@filters = []
|
||||
if match = markup.match(/\s*(#{QuotedFragment})(.*)/)
|
||||
if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
|
||||
@name = match[1]
|
||||
if match[2].match(/#{FilterSeparator}\s*(.*)/)
|
||||
if match[2].match(/#{FilterSeparator}\s*(.*)/o)
|
||||
filters = Regexp.last_match(1).scan(FilterParser)
|
||||
filters.each do |f|
|
||||
if matches = f.match(/\s*(\w+)/)
|
||||
if matches = f.match(/\s*(\w+)(?:\s*#{FilterArgumentSeparator}(.*))?/)
|
||||
filtername = matches[1]
|
||||
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten
|
||||
@filters << [filtername.to_sym, filterargs]
|
||||
filterargs = matches[2].to_s.scan(/(?:\A|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
||||
@filters << [filtername, filterargs]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -36,9 +36,16 @@ module Liquid
|
||||
def render(context)
|
||||
return '' if @name.nil?
|
||||
@filters.inject(context[@name]) do |output, filter|
|
||||
filterargs = filter[1].to_a.collect do |a|
|
||||
context[a]
|
||||
filterargs = []
|
||||
keyword_args = {}
|
||||
filter[1].to_a.each do |a|
|
||||
if matches = a.match(/\A#{TagAttributes}\z/o)
|
||||
keyword_args[matches[1]] = context[matches[2]]
|
||||
else
|
||||
filterargs << context[a]
|
||||
end
|
||||
end
|
||||
filterargs << keyword_args unless keyword_args.empty?
|
||||
begin
|
||||
output = context.invoke(filter[0], output, *filterargs)
|
||||
rescue FilterNotFound
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "liquid"
|
||||
s.version = "2.3.0"
|
||||
s.version = "2.5.0"
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
s.authors = ["Tobias Luetke"]
|
||||
@@ -13,9 +13,12 @@ Gem::Specification.new do |s|
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
s.test_files = Dir.glob("{test}/**/*")
|
||||
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
|
||||
s.files = Dir.glob("{lib}/**/*") +
|
||||
Dir.glob("{ext}/**/*") + %w(MIT-LICENSE README.md)
|
||||
|
||||
s.extensions = ['ext/liquid/extconf.rb']
|
||||
|
||||
s.extra_rdoc_files = ["History.md", "README.md"]
|
||||
|
||||
s.require_path = "lib"
|
||||
s.require_paths = ["lib", "ext"]
|
||||
end
|
||||
|
||||
@@ -4,7 +4,8 @@ require File.dirname(__FILE__) + '/theme_runner'
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.bmbm do |x|
|
||||
x.report("parse & run:") { 10.times { profiler.run(false) } }
|
||||
Benchmark.bm do |x|
|
||||
# x.report("parse:") { 100.times { profiler.compile } }
|
||||
x.report("parse & run:") { 100.times { profiler.run } }
|
||||
end
|
||||
|
||||
|
||||
@@ -6,14 +6,12 @@ profiler = ThemeRunner.new
|
||||
|
||||
puts 'Running profiler...'
|
||||
|
||||
results = profiler.run(true)
|
||||
results = profiler.run_profile
|
||||
|
||||
puts 'Success'
|
||||
puts
|
||||
|
||||
[RubyProf::FlatPrinter, RubyProf::GraphPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter].each do |klass|
|
||||
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/liquid.#{klass.name.downcase}.txt")
|
||||
filename.gsub!(/:+/, '_')
|
||||
File.open(filename, "w+") { |fp| klass.new(results).print(fp) }
|
||||
$stderr.puts "wrote #{klass.name} output to #{filename}"
|
||||
filename = (ENV['TMP'] || '/tmp') + "/callgrind.liquid.txt"
|
||||
File.open(filename, "w+") do |fp|
|
||||
RubyProf::CallTreePrinter.new(results).print(fp, :print_file => true)
|
||||
end
|
||||
$stderr.puts "wrote RubyProf::CallTreePrinter output to #{filename}"
|
||||
|
||||
@@ -27,9 +27,33 @@ class ThemeRunner
|
||||
end.compact
|
||||
end
|
||||
|
||||
def compile
|
||||
# Dup assigns because will make some changes to them
|
||||
|
||||
def run(profile = false)
|
||||
RubyProf.measure_mode = RubyProf::WALL_TIME if profile
|
||||
@tests.each do |liquid, layout, template_name|
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.parse(liquid)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.parse(layout)
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
# Dup assigns because will make some changes to them
|
||||
assigns = Database.tables.dup
|
||||
|
||||
@tests.each do |liquid, layout, template_name|
|
||||
|
||||
# Compute page_tempalte outside of profiler run, uninteresting to profiler
|
||||
page_template = File.basename(template_name, File.extname(template_name))
|
||||
compile_and_render(liquid, layout, assigns, page_template)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def run_profile
|
||||
RubyProf.measure_mode = RubyProf::PROCESS_TIME
|
||||
|
||||
# Dup assigns because will make some changes to them
|
||||
assigns = Database.tables.dup
|
||||
@@ -40,26 +64,27 @@ class ThemeRunner
|
||||
html = nil
|
||||
page_template = File.basename(template_name, File.extname(template_name))
|
||||
|
||||
# Profile compiling and rendering both
|
||||
|
||||
if profile
|
||||
|
||||
RubyProf.resume do
|
||||
html = compile_and_render(liquid, layout, assigns, page_template)
|
||||
end
|
||||
|
||||
else
|
||||
html = compile_and_render(liquid, layout, assigns, page_template)
|
||||
unless @started
|
||||
RubyProf.start
|
||||
RubyProf.pause
|
||||
@started = true
|
||||
end
|
||||
|
||||
html = nil
|
||||
|
||||
RubyProf.resume
|
||||
html = compile_and_render(liquid, layout, assigns, page_template)
|
||||
RubyProf.pause
|
||||
|
||||
|
||||
# return the result and the MD5 of the content, this can be used to detect regressions between liquid version
|
||||
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)] if profile
|
||||
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)]
|
||||
|
||||
# Uncomment to dump html files to /tmp so that you can inspect for errors
|
||||
# File.open("/tmp/#{File.basename(template_name)}.html", "w+") { |fp| fp <<html}
|
||||
end
|
||||
|
||||
RubyProf.stop if profile
|
||||
RubyProf.stop
|
||||
end
|
||||
|
||||
def compile_and_render(template, layout, assigns, page_template)
|
||||
|
||||
@@ -12,4 +12,10 @@ class AssignTest < Test::Unit::TestCase
|
||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
||||
'values' => %w{foo bar baz})
|
||||
end
|
||||
|
||||
def test_assign_with_filter
|
||||
assert_template_result('.bar.',
|
||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
||||
'values' => "foo,bar,baz")
|
||||
end
|
||||
end # AssignTest
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
require 'test_helper'
|
||||
|
||||
class VariableTest < Test::Unit::TestCase
|
||||
class BlockTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
def test_blankspace
|
||||
|
||||
@@ -18,6 +18,11 @@ class ConditionTest < Test::Unit::TestCase
|
||||
assert_evalutes_true '2', '>=', '1'
|
||||
assert_evalutes_true '1', '<=', '2'
|
||||
assert_evalutes_true '1', '<=', '1'
|
||||
# negative numbers
|
||||
assert_evalutes_true '1', '>', '-1'
|
||||
assert_evalutes_true '-1', '<', '1'
|
||||
assert_evalutes_true '1.0', '>', '-1.0'
|
||||
assert_evalutes_true '-1.0', '<', '1.0'
|
||||
end
|
||||
|
||||
def test_default_operators_evalute_false
|
||||
|
||||
@@ -189,10 +189,10 @@ class ContextTest < Test::Unit::TestCase
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
methods_before = context.strainer.methods.map { |method| method.to_s }
|
||||
assert_equal "Wookie", context.invoke("hi", "Wookie")
|
||||
|
||||
context.add_filters(filter)
|
||||
methods_after = context.strainer.methods.map { |method| method.to_s }
|
||||
assert_equal (methods_before + ["hi"]).sort, methods_after.sort
|
||||
assert_equal "Wookie hi!", context.invoke("hi", "Wookie")
|
||||
end
|
||||
|
||||
def test_add_item_in_outer_scope
|
||||
@@ -254,12 +254,16 @@ class ContextTest < Test::Unit::TestCase
|
||||
@context['test'] = {'test' => [1,2,3,4,5]}
|
||||
|
||||
assert_equal 1, @context['test.test[0]']
|
||||
end
|
||||
|
||||
def test_recoursive_array_notation_for_hash
|
||||
@context['test'] = [{'test' => 'worked'}]
|
||||
|
||||
assert_equal 'worked', @context['test[0].test']
|
||||
end
|
||||
|
||||
|
||||
|
||||
def test_hash_to_array_transition
|
||||
@context['colors'] = {
|
||||
'Blue' => ['003366','336699', '6699CC', '99CCFF'],
|
||||
@@ -315,7 +319,7 @@ class ContextTest < Test::Unit::TestCase
|
||||
@context['nested'] = {'var' => 'tags'}
|
||||
@context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
||||
|
||||
assert_equal 'deepsnow', @context['products[var].first']
|
||||
#assert_equal 'deepsnow', @context['products[var].first']
|
||||
assert_equal 'freestyle', @context['products[nested.var].last']
|
||||
end
|
||||
|
||||
|
||||
@@ -115,6 +115,13 @@ class DropsTest < Test::Unit::TestCase
|
||||
assert_equal ' ', output
|
||||
end
|
||||
|
||||
def test_object_methods_not_allowed
|
||||
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
||||
output = Liquid::Template.parse(" {{ product.#{method} }} ").render('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
end
|
||||
end
|
||||
|
||||
def test_scope
|
||||
assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new)
|
||||
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
|
||||
|
||||
@@ -16,6 +16,12 @@ module CanadianMoneyFilter
|
||||
end
|
||||
end
|
||||
|
||||
module SubstituteFilter
|
||||
def substitute(input, params={})
|
||||
input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
|
||||
end
|
||||
end
|
||||
|
||||
class FiltersTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
@@ -75,6 +81,12 @@ class FiltersTest < Test::Unit::TestCase
|
||||
assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
|
||||
end
|
||||
|
||||
def test_strip_html_ignore_comments_with_html
|
||||
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
|
||||
|
||||
assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
|
||||
end
|
||||
|
||||
def test_capitalize
|
||||
@context['var'] = "blub"
|
||||
|
||||
@@ -86,6 +98,13 @@ class FiltersTest < Test::Unit::TestCase
|
||||
|
||||
assert_equal 1000, Variable.new("var | xyzzy").render(@context)
|
||||
end
|
||||
|
||||
def test_filter_with_keyword_arguments
|
||||
@context['surname'] = 'john'
|
||||
@context.add_filters(SubstituteFilter)
|
||||
output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
|
||||
assert_equal 'hello john, doe', output
|
||||
end
|
||||
end
|
||||
|
||||
class FiltersInTemplate < Test::Unit::TestCase
|
||||
|
||||
93
test/liquid/parser_test.rb
Normal file
93
test/liquid/parser_test.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
require 'test_helper'
|
||||
|
||||
|
||||
class ParserTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
|
||||
def test_strings
|
||||
assert_equal [[:id, "string"]], Parser.parse('"string"')
|
||||
assert_equal [[:id, "string"]], Parser.parse('\'string\'')
|
||||
end
|
||||
|
||||
def test_integer
|
||||
assert_equal [[:id, 1]], Parser.parse('1')
|
||||
assert_equal [[:id, 100001]], Parser.parse('100001')
|
||||
end
|
||||
|
||||
def test_float
|
||||
assert_equal [[:id, 1.1]], Parser.parse('1.1')
|
||||
assert_equal [[:id, 1.55435]], Parser.parse('1.55435')
|
||||
end
|
||||
|
||||
def test_null
|
||||
assert_equal [[:id, nil]], Parser.parse('null')
|
||||
assert_equal [[:id, nil]], Parser.parse('nil')
|
||||
end
|
||||
|
||||
def test_bool
|
||||
assert_equal [[:id, true]], Parser.parse('true')
|
||||
assert_equal [[:id, false]], Parser.parse('false')
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
assert_equal [[:id, 1], [:id, 5], [:range, nil]], Parser.parse('(1..5)')
|
||||
assert_equal [[:id, 100], [:id, 500], [:range, nil]], Parser.parse('(100..500)')
|
||||
end
|
||||
|
||||
def test_ranges_with_lookups
|
||||
assert_equal [[:id, 1], [:id, "test"], [:lookup, nil], [:range, nil]], Parser.parse('(1..test)')
|
||||
end
|
||||
|
||||
def test_lookups
|
||||
assert_equal [[:id, "variable"], [:lookup, nil]], Parser.parse('variable')
|
||||
assert_equal [[:id, "underscored_variable"], [:lookup, nil]], Parser.parse('underscored_variable')
|
||||
assert_equal [[:id, "c"], [:lookup, nil]], Parser.parse('c')
|
||||
end
|
||||
|
||||
def test_global_hash
|
||||
assert_equal [[:id, true], [:lookup, nil]], Parser.parse('[true]')
|
||||
|
||||
assert_equal [[:id, "string"], [:lookup, nil]], Parser.parse('["string"]')
|
||||
assert_equal [[:id, 5.55], [:lookup, nil]], Parser.parse('[5.55]')
|
||||
assert_equal [[:id, 0], [:lookup, nil]], Parser.parse('[0]')
|
||||
assert_equal [[:id, "variable"], [:lookup, nil], [:lookup, nil]], Parser.parse('[variable]')
|
||||
end
|
||||
|
||||
def test_descent
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil]], Parser.parse('variable1.variable2')
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil], [:id, "variable3"], [:call, nil]], Parser.parse('variable1.variable2.variable3')
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "under_score"], [:call, nil]], Parser.parse('variable1.under_score')
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "question?"], [:call, nil]], Parser.parse('variable1.question?')
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "exclaimation!"], [:call, nil]], Parser.parse('variable1.exclaimation!')
|
||||
end
|
||||
|
||||
def test_descent_hash
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:call, nil]], Parser.parse('variable1["variable2"]')
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "variable2"], [:lookup, nil], [:call, nil]], Parser.parse('variable1[variable2]')
|
||||
end
|
||||
|
||||
def test_buildin
|
||||
assert_equal [[:id, "first"], [:lookup, nil]], Parser.parse('first')
|
||||
|
||||
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "first"]], Parser.parse('var.first')
|
||||
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "last"]], Parser.parse('var.last')
|
||||
assert_equal [[:id, "var"], [:lookup, nil], [:buildin, "size"]], Parser.parse('var.size')
|
||||
|
||||
end
|
||||
|
||||
def test_descent_hash_descent
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:call, nil], [:call, nil]],
|
||||
Parser.parse('variable1[test1.test2]'), "resolove: variable1[test1.test2]"
|
||||
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:call, nil], [:call, nil]],
|
||||
Parser.parse('variable1[test1["test2"]]'), 'resolove: variable1[test1["test2"]]'
|
||||
|
||||
assert_equal [[:id, "variable1"], [:lookup, nil], [:id, "test1"], [:lookup, nil], [:id, "test2"], [:lookup, nil], [:call, nil], [:call, nil]],
|
||||
Parser.parse('variable1[test1[test2]]'), "resolove: variable1[test1[test2]]"
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
@@ -39,7 +39,7 @@ class ParsingQuirksTest < Test::Unit::TestCase
|
||||
|
||||
def test_meaningless_parens
|
||||
assigns = {'b' => 'bar', 'c' => 'baz'}
|
||||
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
|
||||
markup = "a == 'foo' or b == 'bar' and c == 'baz' or false"
|
||||
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns)
|
||||
end
|
||||
|
||||
|
||||
@@ -38,4 +38,27 @@ class SecurityTest < Test::Unit::TestCase
|
||||
|
||||
assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter)
|
||||
end
|
||||
|
||||
def test_does_not_add_filters_to_symbol_table
|
||||
current_symbols = Symbol.all_symbols
|
||||
|
||||
test = %( {{ "some_string" | a_bad_filter }} )
|
||||
|
||||
template = Template.parse(test)
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
|
||||
template.render
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
|
||||
def test_does_not_add_drop_methods_to_symbol_table
|
||||
current_symbols = Symbol.all_symbols
|
||||
|
||||
drop = Drop.new
|
||||
drop.invoke_drop("custom_method_1")
|
||||
drop.invoke_drop("custom_method_2")
|
||||
drop.invoke_drop("custom_method_3")
|
||||
|
||||
assert_equal [], (Symbol.all_symbols - current_symbols)
|
||||
end
|
||||
end # SecurityTest
|
||||
|
||||
@@ -173,6 +173,10 @@ class StandardFiltersTest < Test::Unit::TestCase
|
||||
assert_template_result "Liquid error: divided by 0", "{{ 5 | divided_by:0 }}"
|
||||
end
|
||||
|
||||
def test_modulo
|
||||
assert_template_result "1", "{{ 3 | modulo:2 }}"
|
||||
end
|
||||
|
||||
def test_append
|
||||
assigns = {'a' => 'bc', 'b' => 'd' }
|
||||
assert_template_result('bcd',"{{ a | append: 'd'}}",assigns)
|
||||
|
||||
@@ -3,23 +3,50 @@ require 'test_helper'
|
||||
class StrainerTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
module AccessScopeFilters
|
||||
def public_filter
|
||||
"public"
|
||||
end
|
||||
|
||||
def private_filter
|
||||
"private"
|
||||
end
|
||||
private :private_filter
|
||||
end
|
||||
|
||||
Strainer.global_filter(AccessScopeFilters)
|
||||
|
||||
def test_strainer
|
||||
strainer = Strainer.create(nil)
|
||||
assert_equal false, strainer.respond_to?('__test__')
|
||||
assert_equal false, strainer.respond_to?('test')
|
||||
assert_equal false, strainer.respond_to?('instance_eval')
|
||||
assert_equal false, strainer.respond_to?('__send__')
|
||||
assert_equal true, strainer.respond_to?('size') # from the standard lib
|
||||
assert_equal 5, strainer.invoke('size', 'input')
|
||||
assert_equal "public", strainer.invoke("public_filter")
|
||||
end
|
||||
|
||||
def test_should_respond_to_two_parameters
|
||||
def test_strainer_only_invokes_public_filter_methods
|
||||
strainer = Strainer.create(nil)
|
||||
assert_equal true, strainer.respond_to?('size', false)
|
||||
assert_equal false, strainer.invokable?('__test__')
|
||||
assert_equal false, strainer.invokable?('test')
|
||||
assert_equal false, strainer.invokable?('instance_eval')
|
||||
assert_equal false, strainer.invokable?('__send__')
|
||||
assert_equal true, strainer.invokable?('size') # from the standard lib
|
||||
end
|
||||
|
||||
# Asserts that Object#respond_to_missing? is not being undefined in Ruby versions where it has been implemented
|
||||
# Currently this method is only present in Ruby v1.9.2, or higher
|
||||
def test_object_respond_to_missing
|
||||
assert_equal Object.respond_to?(:respond_to_missing?), Strainer.create(nil).respond_to?(:respond_to_missing?)
|
||||
def test_strainer_returns_nil_if_no_filter_method_found
|
||||
strainer = Strainer.create(nil)
|
||||
assert_nil strainer.invoke("private_filter")
|
||||
assert_nil strainer.invoke("undef_the_filter")
|
||||
end
|
||||
|
||||
def test_strainer_returns_first_argument_if_no_method_and_arguments_given
|
||||
strainer = Strainer.create(nil)
|
||||
assert_equal "password", strainer.invoke("undef_the_method", "password")
|
||||
end
|
||||
|
||||
def test_strainer_only_allows_methods_defined_in_filters
|
||||
strainer = Strainer.create(nil)
|
||||
assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1")
|
||||
assert_equal "puts", strainer.invoke("__send__", "puts", "Hi Mom")
|
||||
assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke")
|
||||
end
|
||||
|
||||
end # StrainerTest
|
||||
|
||||
16
test/liquid/tags/break_tag_test.rb
Normal file
16
test/liquid/tags/break_tag_test.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
require 'test_helper'
|
||||
|
||||
class BreakTagTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
# tests that no weird errors are raised if break is called outside of a
|
||||
# block
|
||||
def test_break_with_no_block
|
||||
assigns = {'i' => 1}
|
||||
markup = '{% break %}'
|
||||
expected = ''
|
||||
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
end
|
||||
16
test/liquid/tags/continue_tag_test.rb
Normal file
16
test/liquid/tags/continue_tag_test.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ContinueTagTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
# tests that no weird errors are raised if continue is called outside of a
|
||||
# block
|
||||
def test_continue_with_no_block
|
||||
assigns = {}
|
||||
markup = '{% continue %}'
|
||||
expected = ''
|
||||
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
end
|
||||
284
test/liquid/tags/for_tag_test.rb
Normal file
284
test/liquid/tags/for_tag_test.rb
Normal file
@@ -0,0 +1,284 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ForTagTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
def test_for
|
||||
assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4])
|
||||
assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2])
|
||||
assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1])
|
||||
assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2])
|
||||
expected = <<HERE
|
||||
|
||||
yo
|
||||
|
||||
yo
|
||||
|
||||
yo
|
||||
|
||||
HERE
|
||||
template = <<HERE
|
||||
{%for item in array%}
|
||||
yo
|
||||
{%endfor%}
|
||||
HERE
|
||||
assert_template_result(expected,template,'array' => [1,2,3])
|
||||
end
|
||||
|
||||
def test_for_reversed
|
||||
assigns = {'array' => [ 1, 2, 3] }
|
||||
assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns)
|
||||
end
|
||||
|
||||
def test_for_with_range
|
||||
assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}')
|
||||
end
|
||||
|
||||
def test_for_with_variable
|
||||
assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3])
|
||||
assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3])
|
||||
assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3])
|
||||
assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d'])
|
||||
assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c'])
|
||||
assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c'])
|
||||
end
|
||||
|
||||
def test_for_helpers
|
||||
assigns = {'array' => [1,2,3] }
|
||||
assert_template_result(' 1/3 2/3 3/3 ',
|
||||
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
|
||||
assigns)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
|
||||
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
|
||||
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
|
||||
assert_template_result(' 3 2 1 ', '{%for item in array%} {{forloop.rindex}} {%endfor%}', assigns)
|
||||
assert_template_result(' true false false ', '{%for item in array%} {{forloop.first}} {%endfor%}', assigns)
|
||||
assert_template_result(' false false true ', '{%for item in array%} {{forloop.last}} {%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_for_and_if
|
||||
assigns = {'array' => [1,2,3] }
|
||||
assert_template_result('+--',
|
||||
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
|
||||
assigns)
|
||||
end
|
||||
|
||||
def test_for_else
|
||||
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[1,2,3])
|
||||
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[])
|
||||
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>nil)
|
||||
end
|
||||
|
||||
def test_limiting
|
||||
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
|
||||
assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns)
|
||||
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
|
||||
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
|
||||
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_dynamic_variable_limiting
|
||||
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
|
||||
assigns['limit'] = 2
|
||||
assigns['offset'] = 2
|
||||
|
||||
assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_nested_for
|
||||
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
||||
assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_offset_only
|
||||
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
|
||||
assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = <<-MKUP
|
||||
{%for i in array.items limit: 3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
789
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume_limit
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = <<-MKUP
|
||||
{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
7
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume_BIG_limit
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = <<-MKUP
|
||||
{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
7890
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
end
|
||||
|
||||
|
||||
def test_pause_resume_BIG_offset
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%})
|
||||
expected = %q(123
|
||||
next
|
||||
456
|
||||
next
|
||||
)
|
||||
assert_template_result(expected,markup,assigns)
|
||||
end
|
||||
|
||||
def test_for_with_break
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
|
||||
|
||||
markup = '{% for i in array.items %}{% break %}{% endfor %}'
|
||||
expected = ""
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
|
||||
expected = "1"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
|
||||
expected = ""
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
|
||||
expected = "1234"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
# tests to ensure it only breaks out of the local for loop
|
||||
# and not all of them.
|
||||
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
||||
markup = '{% for item in array %}' +
|
||||
'{% for i in item %}' +
|
||||
'{% if i == 1 %}' +
|
||||
'{% break %}' +
|
||||
'{% endif %}' +
|
||||
'{{ i }}' +
|
||||
'{% endfor %}' +
|
||||
'{% endfor %}'
|
||||
expected = '3456'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
# test break does nothing when unreached
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
||||
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
|
||||
expected = '12345'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
def test_for_with_continue
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
||||
|
||||
markup = '{% for i in array.items %}{% continue %}{% endfor %}'
|
||||
expected = ""
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
|
||||
expected = "12345"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
|
||||
expected = ""
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
|
||||
expected = "123"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
|
||||
expected = "1245"
|
||||
assert_template_result(expected,markup,assigns)
|
||||
|
||||
# tests to ensure it only continues the local for loop and not all of them.
|
||||
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
||||
markup = '{% for item in array %}' +
|
||||
'{% for i in item %}' +
|
||||
'{% if i == 1 %}' +
|
||||
'{% continue %}' +
|
||||
'{% endif %}' +
|
||||
'{{ i }}' +
|
||||
'{% endfor %}' +
|
||||
'{% endfor %}'
|
||||
expected = '23456'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
|
||||
# test continue does nothing when unreached
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
||||
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
|
||||
expected = '12345'
|
||||
assert_template_result(expected, markup, assigns)
|
||||
end
|
||||
|
||||
def test_for_tag_string
|
||||
# ruby 1.8.7 "String".each => Enumerator with single "String" element.
|
||||
# ruby 1.9.3 no longer supports .each on String though we mimic
|
||||
# the functionality for backwards compatibility
|
||||
|
||||
assert_template_result('test string',
|
||||
'{%for val in string%}{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
|
||||
assert_template_result('test string',
|
||||
'{%for val in string limit:1%}{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
|
||||
assert_template_result('val-string-1-1-0-1-0-true-true-test string',
|
||||
'{%for val in string%}' +
|
||||
'{{forloop.name}}-' +
|
||||
'{{forloop.index}}-' +
|
||||
'{{forloop.length}}-' +
|
||||
'{{forloop.index0}}-' +
|
||||
'{{forloop.rindex}}-' +
|
||||
'{{forloop.rindex0}}-' +
|
||||
'{{forloop.first}}-' +
|
||||
'{{forloop.last}}-' +
|
||||
'{{val}}{%endfor%}',
|
||||
'string' => "test string")
|
||||
end
|
||||
|
||||
def test_blank_string_not_iterable
|
||||
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '')
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,18 @@ require 'test_helper'
|
||||
class HtmlTagTest < Test::Unit::TestCase
|
||||
include Liquid
|
||||
|
||||
class ArrayDrop < Liquid::Drop
|
||||
include Enumerable
|
||||
|
||||
def initialize(array)
|
||||
@array = array
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@array.each(&block)
|
||||
end
|
||||
end
|
||||
|
||||
def test_html_table
|
||||
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
@@ -26,4 +38,26 @@ class HtmlTagTest < Test::Unit::TestCase
|
||||
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
|
||||
'numbers' => [1,2,3,4,5,6])
|
||||
end
|
||||
|
||||
def test_quoted_fragment
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
"{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}",
|
||||
'collections' => {'frontpage' => [1,2,3,4,5,6]})
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
"{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}",
|
||||
'collections' => {'frontpage' => [1,2,3,4,5,6]})
|
||||
|
||||
end
|
||||
|
||||
def test_enumerable_drop
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
|
||||
'numbers' => ArrayDrop.new([1,2,3,4,5,6]))
|
||||
end
|
||||
|
||||
def test_offset_and_limit
|
||||
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
|
||||
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
|
||||
'numbers' => [0,1,2,3,4,5,6,7])
|
||||
end
|
||||
end # HtmlTagTest
|
||||
|
||||
@@ -47,166 +47,6 @@ class StandardTagTest < Test::Unit::TestCase
|
||||
{%endcomment%}bar')
|
||||
end
|
||||
|
||||
def test_for
|
||||
assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4])
|
||||
assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2])
|
||||
assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1])
|
||||
assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2])
|
||||
expected = <<HERE
|
||||
|
||||
yo
|
||||
|
||||
yo
|
||||
|
||||
yo
|
||||
|
||||
HERE
|
||||
template = <<HERE
|
||||
{%for item in array%}
|
||||
yo
|
||||
{%endfor%}
|
||||
HERE
|
||||
assert_template_result(expected,template,'array' => [1,2,3])
|
||||
end
|
||||
|
||||
def test_for_with_range
|
||||
assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}')
|
||||
end
|
||||
|
||||
def test_for_with_variable
|
||||
assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3])
|
||||
assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3])
|
||||
assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3])
|
||||
assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d'])
|
||||
assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c'])
|
||||
assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c'])
|
||||
end
|
||||
|
||||
def test_for_helpers
|
||||
assigns = {'array' => [1,2,3] }
|
||||
assert_template_result(' 1/3 2/3 3/3 ',
|
||||
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
|
||||
assigns)
|
||||
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
|
||||
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
|
||||
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
|
||||
assert_template_result(' 3 2 1 ', '{%for item in array%} {{forloop.rindex}} {%endfor%}', assigns)
|
||||
assert_template_result(' true false false ', '{%for item in array%} {{forloop.first}} {%endfor%}', assigns)
|
||||
assert_template_result(' false false true ', '{%for item in array%} {{forloop.last}} {%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_for_and_if
|
||||
assigns = {'array' => [1,2,3] }
|
||||
assert_template_result('+--',
|
||||
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
|
||||
assigns)
|
||||
end
|
||||
|
||||
def test_for_else
|
||||
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[1,2,3])
|
||||
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>[])
|
||||
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array'=>nil)
|
||||
end
|
||||
|
||||
def test_limiting
|
||||
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
|
||||
assert_template_result('12', '{%for i in array limit:2 %}{{ i }}{%endfor%}', assigns)
|
||||
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
|
||||
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
|
||||
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_dynamic_variable_limiting
|
||||
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
|
||||
assigns['limit'] = 2
|
||||
assigns['offset'] = 2
|
||||
|
||||
assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_nested_for
|
||||
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
||||
assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_offset_only
|
||||
assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]}
|
||||
assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = <<-MKUP
|
||||
{%for i in array.items limit: 3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
789
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume_limit
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = <<-MKUP
|
||||
{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
7
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
end
|
||||
|
||||
def test_pause_resume_BIG_limit
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = <<-MKUP
|
||||
{%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%}
|
||||
MKUP
|
||||
expected = <<-XPCTD
|
||||
123
|
||||
next
|
||||
456
|
||||
next
|
||||
7890
|
||||
XPCTD
|
||||
assert_template_result(expected,markup,assigns)
|
||||
end
|
||||
|
||||
|
||||
def test_pause_resume_BIG_offset
|
||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}}
|
||||
markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
|
||||
next
|
||||
{%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%})
|
||||
expected = %q(123
|
||||
next
|
||||
456
|
||||
next
|
||||
)
|
||||
assert_template_result(expected,markup,assigns)
|
||||
end
|
||||
|
||||
def test_assign
|
||||
assigns = {'var' => 'content' }
|
||||
assert_template_result('var2: var2:content', 'var2:{{var2}} {%assign var2 = var%} var2:{{var2}}', assigns)
|
||||
@@ -445,12 +285,6 @@ HERE
|
||||
assert_template_result('', '{% if null == true %}?{% endif %}', {})
|
||||
end
|
||||
|
||||
def test_for_reversed
|
||||
assigns = {'array' => [ 1, 2, 3] }
|
||||
assert_template_result('321','{%for item in array reversed %}{{item}}{%endfor%}',assigns)
|
||||
end
|
||||
|
||||
|
||||
def test_ifchanged
|
||||
assigns = {'array' => [ 1, 1, 2, 2, 3, 3] }
|
||||
assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)
|
||||
|
||||
@@ -11,67 +11,71 @@ class VariableTest < Test::Unit::TestCase
|
||||
def test_filters
|
||||
var = Variable.new('hello | textileze')
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [[:textileze,[]]], var.filters
|
||||
assert_equal [["textileze",[]]], var.filters
|
||||
|
||||
var = Variable.new('hello | textileze | paragraph')
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters
|
||||
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
|
||||
|
||||
var = Variable.new(%! hello | strftime: '%Y'!)
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [[:strftime,["'%Y'"]]], var.filters
|
||||
assert_equal [["strftime",["'%Y'"]]], var.filters
|
||||
|
||||
var = Variable.new(%! 'typo' | link_to: 'Typo', true !)
|
||||
assert_equal %!'typo'!, var.name
|
||||
assert_equal [[:link_to,["'Typo'", "true"]]], var.filters
|
||||
assert_equal [["link_to",["'Typo'", "true"]]], var.filters
|
||||
|
||||
var = Variable.new(%! 'typo' | link_to: 'Typo', false !)
|
||||
assert_equal %!'typo'!, var.name
|
||||
assert_equal [[:link_to,["'Typo'", "false"]]], var.filters
|
||||
assert_equal [["link_to",["'Typo'", "false"]]], var.filters
|
||||
|
||||
var = Variable.new(%! 'foo' | repeat: 3 !)
|
||||
assert_equal %!'foo'!, var.name
|
||||
assert_equal [[:repeat,["3"]]], var.filters
|
||||
assert_equal [["repeat",["3"]]], var.filters
|
||||
|
||||
var = Variable.new(%! 'foo' | repeat: 3, 3 !)
|
||||
assert_equal %!'foo'!, var.name
|
||||
assert_equal [[:repeat,["3","3"]]], var.filters
|
||||
assert_equal [["repeat",["3","3"]]], var.filters
|
||||
|
||||
var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !)
|
||||
assert_equal %!'foo'!, var.name
|
||||
assert_equal [[:repeat,["3","3","3"]]], var.filters
|
||||
assert_equal [["repeat",["3","3","3"]]], var.filters
|
||||
|
||||
var = Variable.new(%! hello | strftime: '%Y, okay?'!)
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [[:strftime,["'%Y, okay?'"]]], var.filters
|
||||
assert_equal [["strftime",["'%Y, okay?'"]]], var.filters
|
||||
|
||||
var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!)
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [[:things,["\"%Y, okay?\"","'the other one'"]]], var.filters
|
||||
assert_equal [["things",["\"%Y, okay?\"","'the other one'"]]], var.filters
|
||||
end
|
||||
|
||||
def test_filter_with_date_parameter
|
||||
|
||||
var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!)
|
||||
assert_equal "'2006-06-06'", var.name
|
||||
assert_equal [[:date,["\"%m/%d/%Y\""]]], var.filters
|
||||
assert_equal [["date",["\"%m/%d/%Y\""]]], var.filters
|
||||
|
||||
end
|
||||
|
||||
def test_filters_without_whitespace
|
||||
var = Variable.new('hello | textileze | paragraph')
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters
|
||||
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
|
||||
|
||||
var = Variable.new('hello|textileze|paragraph')
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters
|
||||
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
|
||||
|
||||
var = Variable.new("hello|replace:'foo','bar'|textileze")
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [["replace", ["'foo'", "'bar'"]], ["textileze", []]], var.filters
|
||||
end
|
||||
|
||||
def test_symbol
|
||||
var = Variable.new("http://disney.com/logo.gif | image: 'med' ")
|
||||
assert_equal 'http://disney.com/logo.gif', var.name
|
||||
assert_equal [[:image,["'med'"]]], var.filters
|
||||
assert_equal [["image",["'med'"]]], var.filters
|
||||
end
|
||||
|
||||
def test_string_single_quoted
|
||||
@@ -103,6 +107,12 @@ class VariableTest < Test::Unit::TestCase
|
||||
var = Variable.new(%| test.test |)
|
||||
assert_equal 'test.test', var.name
|
||||
end
|
||||
|
||||
def test_filter_with_keyword_arguments
|
||||
var = Variable.new(%! hello | things: greeting: "world", farewell: 'goodbye'!)
|
||||
assert_equal 'hello', var.name
|
||||
assert_equal [['things',["greeting: \"world\"","farewell: 'goodbye'"]]], var.filters
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user