Compare commits

..

6 Commits

Author SHA1 Message Date
Arthur Neves
a41a60d294 Bump version to rc1 2013-10-10 10:22:35 -04:00
Florian Weingarten
307cab2eaa History and gemspec
Conflicts:
	History.md
	liquid.gemspec
2013-10-09 17:04:47 -04:00
Arthur Neves
a64360d148 Update history
Conflicts:
	History.md
2013-10-09 16:33:44 -04:00
Florian Weingarten
6cb5a9b7cc Merge pull request #234 from Shopify/fix_mapping_procs
Fix mapping over procs
2013-10-09 16:32:53 -04:00
Florian Weingarten
4207d1f086 Merge pull request #232 from Shopify/to_liquid_stuff
Always call 'to_liquid' on stuff in map filter and allow to_liquid to be...
2013-10-09 16:32:45 -04:00
Florian Weingarten
988a1694fd Use invoke_drop in map filter 2013-10-09 16:32:38 -04:00
80 changed files with 304 additions and 2199 deletions

3
.gitignore vendored
View File

@@ -5,6 +5,3 @@ pkg
*.rbc
.rvmrc
.ruby-version
*.bundle
/tmp
Gemfile.lock

View File

@@ -1,14 +1,11 @@
rvm:
- 1.8.7
- 1.9.3
- 2.0.0
- 2.1.0
- ree
- jruby-18mode
- jruby-19mode
- jruby-head
- rbx-18mode
- rbx-19mode
matrix:
allow_failures:
- rvm: rbx-19mode
- rvm: jruby-head
script: "rake test"

View File

@@ -1,3 +0,0 @@
source 'https://rubygems.org'
gemspec

View File

@@ -1,33 +1,11 @@
# Liquid Version History
## 3.0.0 / not yet released / branch "master"
* ...
* Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
* Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
* Fix resource counting bug with respond_to?(:length), see #263 [Florian Weingarten, fw42]
* Allow specifying custom patterns for template filenames, see #284 [Andrei Gladkyi, agladkyi]
* Allow drops to optimize loading a slice of elements, see #282 [Tom Burns, boourns]
* Support for passing variables to snippets in subdirs, see #271 [Joost Hietbrink, joost]
* Add a class cache to avoid runtime extend calls, see #249 [James Tucker, raggi]
* Remove some legacy Ruby 1.8 compatibility code, see #276 [Florian Weingarten, fw42]
* Add default filter to standard filters, see #267 [Derrick Reimer, djreimer]
* Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume]
* Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42]
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
## 2.6.0 / 2013-11-25 / branch "2.6-stable"
IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
## 2.6.0 (not yet released)
* ...
* Bugfix for #106: fix example servlet [gnowoel]
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss]
* Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
@@ -36,7 +14,6 @@ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42]
* Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet]
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet]
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [wǒ_is神仙, jsw0528]
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep, darkhelmet]
* Resource limits [Florian Weingarten, fw42]
* Add reverse filter [Jay Strybis, unreal]
@@ -47,11 +24,7 @@ The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are
* Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn]
## 2.5.4 / 2013-11-11 / branch "2.5-stable"
* Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528]
## 2.5.3 / 2013-10-09
## 2.5.3 / branch "2.5-stable"
* #232, #234, #237: Fix map filter bugs [Florian Weingarten, fw42]

View File

@@ -1,10 +1,9 @@
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png?branch=master)](http://travis-ci.org/Shopify/liquid)
# Liquid template engine
* [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md)
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Liquid Wiki from Shopify](http://wiki.shopify.com/Liquid)
* [Website](http://liquidmarkup.org/)
## Introduction
@@ -48,26 +47,4 @@ For standard use you can just pass it the content of a file and call render with
@template.render('name' => 'tobi') # => "hi tobi"
```
### Error Modes
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
it very hard to debug and can lead to unexpected behaviour.
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
when templates are invalid. You can enable this new parser like this:
```ruby
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
```ruby
Liquid::Template.parse(source, :error_mode => :strict)
```
This is useful for doing things like enabling strict mode only in the theme editor.
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
[![Build Status](https://secure.travis-ci.org/Shopify/liquid.png)](http://travis-ci.org/Shopify/liquid)

View File

@@ -1,60 +1,35 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/extensiontask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version"
require 'rubygems/package_task'
task :default => 'test'
desc 'run test suite with default parser'
Rake::TestTask.new(:base_test) do |t|
Rake::TestTask.new(:test) do |t|
t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/liquid/**/*_test.rb']
t.verbose = false
end
desc 'run test suite with warn error mode'
task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke
gemspec = eval(File.read('liquid.gemspec'))
Gem::PackageTask.new(gemspec) do |pkg|
pkg.gem_spec = gemspec
end
desc 'runs test suite with both strict and lax parsers'
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
end
task :gem => :build
task :build do
system "gem build liquid.gemspec"
end
task :install => :build do
system "gem install liquid-#{Liquid::VERSION}.gem"
end
task :release => :build do
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
system "git push --tags"
system "gem push liquid-#{Liquid::VERSION}.gem"
system "rm liquid-#{Liquid::VERSION}.gem"
desc "Build the gem and release it to rubygems.org"
task :release => :gem do
sh "gem push pkg/liquid-#{gemspec.version}.gem"
end
namespace :benchmark do
desc "Run the liquid benchmark with lax parsing"
desc "Run the liquid benchmark"
task :run do
ruby "./performance/benchmark.rb lax"
ruby "./performance/benchmark.rb"
end
desc "Run the liquid benchmark with strict parsing"
task :strict do
ruby "./performance/benchmark.rb strict"
end
end
@@ -65,10 +40,6 @@ namespace :profile do
ruby "./performance/profile.rb"
end
task :stackprof do
ruby "./performance/stackprof.rb"
end
desc "Run KCacheGrind"
task :grind => :run do
system "qcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
@@ -80,8 +51,3 @@ desc "Run example"
task :example do
ruby "-w -d -Ilib example/server/server.rb"
end
Rake::ExtensionTask.new "liquid" do |ext|
ext.lib_dir = "lib/liquid"
end
Rake::Task[:test].prerequisites << :compile

View File

@@ -13,7 +13,7 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def handle(type, req, res)
@request, @response = req, res
@request.path_info =~ /(\w+)\z/
@request.path_info =~ /(\w+)$/
@action = $1 || 'index'
@assigns = send(@action) if respond_to?(@action)

View File

@@ -1,167 +0,0 @@
#include "liquid_ext.h"
VALUE cLiquidBlock;
ID intern_assert_missing_delimitation, intern_block_delimiter, intern_is_blank,
intern_new_with_options, intern_tags, intern_unknown_tag, intern_unterminated_tag,
intern_unterminated_variable;
struct liquid_tag
{
char *name, *markup;
long name_length, markup_length;
};
static bool parse_tag(struct liquid_tag *tag, char *token, long token_length)
{
// Strip {{ and }} braces
token += 2;
token_length -= 4;
char *end = token + token_length;
while (token < end && isspace(*token))
token++;
tag->name = token;
char c = *token;
while (token < end && (isalnum(c) || c == '_'))
c = *(++token);
tag->name_length = token - tag->name;
if (!tag->name_length) {
memset(tag, 0, sizeof(*tag));
return false;
}
while (token < end && isspace(*token))
token++;
tag->markup = token;
char *last = end - 1;
while (token < last && isspace(*last))
last--;
end = last + 1;
tag->markup_length = end - token;
return true;
}
static VALUE rb_parse_body(VALUE self, VALUE tokenizerObj)
{
struct liquid_tokenizer *tokenizer = LIQUID_TOKENIZER_GET_STRUCT(tokenizerObj);
bool blank = true;
VALUE nodelist = rb_iv_get(self, "@nodelist");
if (nodelist == Qnil) {
nodelist = rb_ary_new();
rb_iv_set(self, "@nodelist", nodelist);
} else {
rb_ary_clear(nodelist);
}
struct token token;
while (true) {
liquid_tokenizer_next(tokenizer, &token);
switch (token.type) {
case TOKEN_NONE:
/*
* Make sure that it's ok to end parsing in the current block.
* Effectively this method will throw an exception unless the current block is
* of type Document
*/
rb_funcall(self, intern_assert_missing_delimitation, 0);
goto done;
case TOKEN_INVALID:
{
VALUE token_obj = rb_str_new(token.str, token.length);
if (token.str[1] == '%')
rb_funcall(self, intern_unterminated_tag, 1, token_obj);
else
rb_funcall(self, intern_unterminated_variable, 1, token_obj);
break;
}
case TOKEN_TAG:
{
struct liquid_tag tag;
if (!parse_tag(&tag, token.str, token.length)) {
// FIXME: provide more appropriate error message
rb_funcall(self, intern_unterminated_tag, 1, rb_str_new(token.str, token.length));
} else {
if (tag.name_length >= 3 && !memcmp(tag.name, "end", 3)) {
VALUE block_delimiter = rb_funcall(self, intern_block_delimiter, 0);
if (TYPE(block_delimiter) == T_STRING &&
tag.name_length == RSTRING_LEN(block_delimiter) &&
!memcmp(tag.name, RSTRING_PTR(block_delimiter), tag.name_length))
{
goto done;
}
}
VALUE tags = rb_funcall(cLiquidTemplate, intern_tags, 0);
Check_Type(tags, T_HASH);
VALUE tag_name = rb_str_new(tag.name, tag.name_length);
VALUE tag_class = rb_hash_lookup(tags, tag_name);
VALUE markup = rb_str_new(tag.markup, tag.markup_length);
if (tag_class != Qnil) {
VALUE options = rb_iv_get(self, "@options");
if (options == Qnil)
options = rb_hash_new();
VALUE new_tag = rb_funcall(tag_class, intern_new_with_options, 4,
tag_name, markup, tokenizerObj, options);
if (blank) {
VALUE blank_block = rb_funcall(new_tag, intern_is_blank, 0);
if (blank_block == Qnil || blank_block == Qfalse)
blank = false;
}
rb_ary_push(nodelist, new_tag);
} else {
rb_funcall(self, intern_unknown_tag, 3, tag_name, markup, tokenizerObj);
/*
* multi-block tags may store the nodelist in a block array on unknown_tag
* then replace @nodelist with a new array. We need to use the new array
* for the block following the tag token.
*/
nodelist = rb_iv_get(self, "@nodelist");
}
}
break;
}
case TOKEN_VARIABLE:
{
VALUE markup = rb_str_new(token.str + 2, token.length - 4);
VALUE options = rb_iv_get(self, "@options");
VALUE new_var = rb_funcall(cLiquidVariable, intern_new, 2, markup, options);
rb_ary_push(nodelist, new_var);
blank = false;
break;
}
case TOKEN_STRING:
rb_ary_push(nodelist, liquid_string_slice_new(token.str, token.length));
if (blank) {
int i;
for (i = 0; i < token.length; i++) {
if (!isspace(token.str[i])) {
blank = false;
break;
}
}
}
break;
}
}
done:
rb_iv_set(self, "@blank", blank ? Qtrue : Qfalse);
return Qnil;
}
void init_liquid_block()
{
intern_assert_missing_delimitation = rb_intern("assert_missing_delimitation!");
intern_block_delimiter = rb_intern("block_delimiter");
intern_is_blank = rb_intern("blank?");
intern_new_with_options = rb_intern("new_with_options");
intern_tags = rb_intern("tags");
intern_unknown_tag = rb_intern("unknown_tag");
intern_unterminated_tag = rb_intern("unterminated_tag");
intern_unterminated_variable = rb_intern("unterminated_variable");
cLiquidBlock = rb_define_class_under(mLiquid, "Block", cLiquidTag);
rb_define_method(cLiquidBlock, "parse_body", rb_parse_body, 1);
}

View File

@@ -1,8 +0,0 @@
#ifndef LIQUID_BLOCK_H
#define LIQUID_BLOCK_H
void init_liquid_block();
extern VALUE cLiquidBlock;
#endif

View File

@@ -1,3 +0,0 @@
require 'mkmf'
$CFLAGS << ' -Wall'
create_makefile("liquid/liquid")

View File

@@ -1,18 +0,0 @@
#include "liquid_ext.h"
VALUE mLiquid;
VALUE cLiquidTemplate, cLiquidTag, cLiquidVariable;
ID intern_new;
void Init_liquid(void)
{
intern_new = rb_intern("new");
mLiquid = rb_define_module("Liquid");
cLiquidTemplate = rb_define_class_under(mLiquid, "Template", rb_cObject);
cLiquidTag = rb_define_class_under(mLiquid, "Tag", rb_cObject);
cLiquidVariable = rb_define_class_under(mLiquid, "Variable", rb_cObject);
init_liquid_tokenizer();
init_liquid_block();
init_liquid_string_slice();
}

View File

@@ -1,17 +0,0 @@
#ifndef LIQUID_EXT_H
#define LIQUID_EXT_H
#include <stdbool.h>
#include <ctype.h>
#include <ruby.h>
#include "tokenizer.h"
#include "block.h"
#include "slice.h"
#include "utils.h"
extern ID intern_new;
extern VALUE mLiquid;
extern VALUE cLiquidTemplate, cLiquidTag, cLiquidVariable;
#endif

View File

@@ -1,167 +0,0 @@
#include "liquid_ext.h"
VALUE cLiquidStringSlice;
static void mark_slice(void *ptr)
{
if (!ptr)
return;
struct string_slice *slice = ptr;
rb_gc_mark(slice->source);
}
static void free_slice(void *ptr)
{
struct string_slice *slice = ptr;
xfree(slice);
}
VALUE liquid_string_slice_new(const char *str, long length)
{
return rb_funcall(cLiquidStringSlice, intern_new, 3, rb_str_new(str, length), INT2FIX(0), INT2FIX(length));
}
static VALUE rb_allocate(VALUE klass)
{
struct string_slice *slice;
VALUE obj = Data_Make_Struct(klass, struct string_slice, mark_slice, free_slice, slice);
return obj;
}
static VALUE rb_initialize(VALUE self, VALUE source, VALUE offset_value, VALUE length_value)
{
long offset = rb_fix2int(offset_value);
long length = rb_fix2int(length_value);
if (length < 0)
rb_raise(rb_eArgError, "negative string length");
if (offset < 0)
rb_raise(rb_eArgError, "negative string offset");
if (TYPE(source) == T_DATA && RBASIC_CLASS(source) == cLiquidStringSlice) {
struct string_slice *source_slice = DATA_PTR(source);
source = source_slice->source;
offset += source_slice->str - RSTRING_PTR(source);
} else {
source = rb_string_value(&source);
source = rb_str_dup_frozen(source);
}
struct string_slice *slice;
Data_Get_Struct(self, struct string_slice, slice);
slice->source = source;
slice->str = RSTRING_PTR(source) + offset;
slice->length = length;
if (length > RSTRING_LEN(source) - offset)
rb_raise(rb_eArgError, "slice bounds outside source string bounds");
return Qnil;
}
static VALUE rb_slice_to_str(VALUE self)
{
struct string_slice *slice;
Data_Get_Struct(self, struct string_slice, slice);
VALUE source = slice->source;
if (slice->str == RSTRING_PTR(source) && slice->length == RSTRING_LEN(source))
return source;
source = rb_str_new(slice->str, slice->length);
slice->source = source;
slice->str = RSTRING_PTR(source);
return source;
}
static VALUE rb_slice_slice(VALUE self, VALUE offset, VALUE length)
{
return rb_funcall(cLiquidStringSlice, intern_new, 3, self, offset, length);
}
static VALUE rb_slice_length(VALUE self)
{
struct string_slice *slice;
Data_Get_Struct(self, struct string_slice, slice);
return INT2FIX(slice->length);
}
static VALUE rb_slice_equal(VALUE self, VALUE other)
{
struct string_slice *this_slice;
Data_Get_Struct(self, struct string_slice, this_slice);
const char *other_str;
long other_length;
if (TYPE(other) == T_DATA && RBASIC_CLASS(other) == cLiquidStringSlice) {
struct string_slice *other_slice = DATA_PTR(other);
other_str = other_slice->str;
other_length = other_slice->length;
} else {
other = rb_string_value(&other);
other_length = RSTRING_LEN(other);
other_str = RSTRING_PTR(other);
}
bool equal = this_slice->length == other_length && !memcmp(this_slice->str, other_str, other_length);
return equal ? Qtrue : Qfalse;
}
static VALUE rb_slice_inspect(VALUE self)
{
VALUE quoted = rb_str_inspect(rb_slice_to_str(self));
return rb_sprintf("#<Liquid::StringSlice: %.*s>", (int)RSTRING_LEN(quoted), RSTRING_PTR(quoted));
}
static VALUE rb_slice_join(VALUE klass, VALUE ary)
{
ary = rb_ary_to_ary(ary);
long i;
long result_length = 0;
for (i = 0; i < RARRAY_LEN(ary); i++) {
VALUE element = RARRAY_AREF(ary, i);
if (TYPE(element) == T_DATA && RBASIC_CLASS(element) == cLiquidStringSlice) {
struct string_slice *slice = DATA_PTR(element);
result_length += slice->length;
} else if (TYPE(element) == T_STRING) {
result_length += RSTRING_LEN(element);
}
}
VALUE result = rb_str_buf_new(result_length);
for (i = 0; i < RARRAY_LEN(ary); i++) {
VALUE element = RARRAY_AREF(ary, i);
const char *element_string;
long element_length;
if (TYPE(element) == T_DATA && RBASIC_CLASS(element) == cLiquidStringSlice) {
struct string_slice *slice = DATA_PTR(element);
element_string = slice->str;
element_length = slice->length;
} else if (NIL_P(element)) {
continue;
} else {
element = rb_check_string_type(element);
if (NIL_P(element))
continue;
element_string = RSTRING_PTR(element);
element_length = RSTRING_LEN(element);
}
rb_str_buf_cat(result, element_string, element_length);
}
return result;
}
void init_liquid_string_slice()
{
cLiquidStringSlice = rb_define_class_under(mLiquid, "StringSlice", rb_cObject);
rb_define_singleton_method(cLiquidStringSlice, "join", rb_slice_join, 1);
rb_define_alloc_func(cLiquidStringSlice, rb_allocate);
rb_define_method(cLiquidStringSlice, "initialize", rb_initialize, 3);
rb_define_method(cLiquidStringSlice, "==", rb_slice_equal, 1);
rb_define_method(cLiquidStringSlice, "length", rb_slice_length, 0);
rb_define_alias(cLiquidStringSlice, "size", "length");
rb_define_method(cLiquidStringSlice, "slice", rb_slice_slice, 2);
rb_define_method(cLiquidStringSlice, "to_str", rb_slice_to_str, 0);
rb_define_alias(cLiquidStringSlice, "to_s", "to_str");
rb_define_method(cLiquidStringSlice, "inspect", rb_slice_inspect, 0);
}

View File

@@ -1,18 +0,0 @@
#ifndef LIQUID_SLICE_H
#define LIQUID_SLICE_H
extern VALUE cLiquidStringSlice;
struct string_slice {
VALUE source;
const char *str;
long length;
};
VALUE liquid_string_slice_new(const char *str, long length);
void init_liquid_string_slice();
#define STRING_SLICE_GET_STRUCT(obj) ((struct string_slice *)obj_get_data_ptr(obj, cLiquidStringSlice))
#endif

View File

@@ -1,113 +0,0 @@
#include "liquid_ext.h"
VALUE cLiquidTokenizer;
static void free_tokenizer(void *ptr)
{
struct liquid_tokenizer *tokenizer = ptr;
xfree(tokenizer);
}
static VALUE rb_allocate(VALUE klass)
{
VALUE obj;
struct liquid_tokenizer *tokenizer;
obj = Data_Make_Struct(klass, struct liquid_tokenizer, NULL, free_tokenizer, tokenizer);
return obj;
}
static VALUE rb_initialize(VALUE self, VALUE source)
{
struct liquid_tokenizer *tokenizer;
source = rb_string_value(&source);
Data_Get_Struct(self, struct liquid_tokenizer, tokenizer);
tokenizer->cursor = RSTRING_PTR(source);
tokenizer->length = RSTRING_LEN(source);
return Qnil;
}
void liquid_tokenizer_next(struct liquid_tokenizer *tokenizer, struct token *token)
{
if (tokenizer->length <= 0) {
memset(token, 0, sizeof(*token));
return;
}
token->type = TOKEN_STRING;
char *cursor = tokenizer->cursor;
char *last = tokenizer->cursor + tokenizer->length - 1;
while (cursor < last) {
if (*cursor++ != '{')
continue;
char c = *cursor++;
if (c != '%' && c != '{')
continue;
if (cursor - tokenizer->cursor > 2) {
token->type = TOKEN_STRING;
cursor -= 2;
goto found;
}
char *incomplete_end = cursor;
token->type = TOKEN_INVALID;
if (c == '%') {
while (cursor < last) {
if (*cursor++ != '%')
continue;
c = *cursor++;
while (c == '%' && cursor <= last)
c = *cursor++;
if (c != '}')
continue;
token->type = TOKEN_TAG;
goto found;
}
cursor = incomplete_end;
goto found;
} else {
while (cursor < last) {
if (*cursor++ != '}')
continue;
if (*cursor++ != '}') {
incomplete_end = cursor - 1;
continue;
}
token->type = TOKEN_VARIABLE;
goto found;
}
cursor = incomplete_end;
goto found;
}
}
cursor = last + 1;
found:
token->str = tokenizer->cursor;
token->length = cursor - tokenizer->cursor;
tokenizer->cursor += token->length;
tokenizer->length -= token->length;
}
static VALUE rb_next(VALUE self)
{
struct liquid_tokenizer *tokenizer;
Data_Get_Struct(self, struct liquid_tokenizer, tokenizer);
struct token token;
liquid_tokenizer_next(tokenizer, &token);
if (token.type == TOKEN_NONE)
return Qnil;
return rb_str_new(token.str, token.length);
}
void init_liquid_tokenizer()
{
cLiquidTokenizer = rb_define_class_under(mLiquid, "Tokenizer", rb_cObject);
rb_define_alloc_func(cLiquidTokenizer, rb_allocate);
rb_define_method(cLiquidTokenizer, "initialize", rb_initialize, 1);
rb_define_method(cLiquidTokenizer, "next", rb_next, 0);
rb_define_alias(cLiquidTokenizer, "shift", "next");
}

View File

@@ -1,30 +0,0 @@
#ifndef LIQUID_TOKENIZER_H
#define LIQUID_TOKENIZER_H
extern VALUE cLiquidTokenizer;
enum token_type {
TOKEN_NONE,
TOKEN_INVALID,
TOKEN_STRING,
TOKEN_TAG,
TOKEN_VARIABLE
};
struct token {
enum token_type type;
char *str;
int length;
};
struct liquid_tokenizer {
char *cursor;
int length;
};
void init_liquid_tokenizer();
void liquid_tokenizer_next(struct liquid_tokenizer *tokenizer, struct token *token);
#define LIQUID_TOKENIZER_GET_STRUCT(obj) ((struct liquid_tokenizer *)obj_get_data_ptr(obj, cLiquidTokenizer))
#endif

View File

@@ -1,21 +0,0 @@
#include <ruby.h>
void raise_type_error(VALUE expected, VALUE got)
{
rb_raise(rb_eTypeError, "wrong argument type %s (expected %s)",
rb_class2name(got), rb_class2name(expected));
}
void check_class(VALUE obj, int type, VALUE klass)
{
Check_Type(obj, type);
VALUE obj_klass = RBASIC_CLASS(obj);
if (obj_klass != klass)
raise_type_error(klass, obj_klass);
}
void *obj_get_data_ptr(VALUE obj, VALUE klass)
{
check_class(obj, T_DATA, klass);
return DATA_PTR(obj);
}

View File

@@ -1,8 +0,0 @@
#ifndef LIQUID_UTILS_H
#define LIQUID_UTILS_H
void raise_type_error(VALUE expected, VALUE got);
void check_class(VALUE klass);
void *obj_get_data_ptr(VALUE obj, VALUE klass);
#endif

View File

@@ -30,22 +30,22 @@ module Liquid
VariableSegment = /[\w\-]/
VariableStart = /\{\{/
VariableEnd = /\}\}/
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s|:,]+/
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/o
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/o
SpacelessFilter = /\A(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/o
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/o
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
end
require 'liquid/liquid'
require "liquid/version"
require 'liquid/lexer'
require 'liquid/parser'
require 'liquid/i18n'
require 'liquid/drop'
require 'liquid/extensions'
require 'liquid/errors'

View File

@@ -1,37 +1,65 @@
module Liquid
class Block < Tag
def initialize(tag_name, markup, tokens)
super
parse_body(tokens)
end
IsTag = /^#{TagStart}/o
IsVariable = /^#{VariableStart}/o
FullToken = /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
ContentOfVariable = /^#{VariableStart}(.*)#{VariableEnd}$/o
def blank?
@blank || false
end
def parse(tokens)
@nodelist ||= []
@nodelist.clear
# warnings of this block and all sub-tags
def warnings
all_warnings = []
all_warnings.concat(@warnings) if @warnings
while token = tokens.shift
(nodelist || []).each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
case token
when IsTag
if token =~ FullToken
# if we found the proper block delimiter 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 IsVariable
@nodelist << create_variable(token)
when ''
# pass
else
@nodelist << token
end
end
all_warnings
# Make sure that it's ok to end parsing in the current block.
# Effectively this method will throw an 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.new(options[:locale].t("errors.syntax.unexpected_else",
:block_name => block_name))
raise SyntaxError, "#{block_name} tag does not expect else tag"
when 'end'
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter",
:block_name => block_name,
:block_delimiter => block_delimiter))
raise SyntaxError, "'end' is not a valid delimiter for #{block_name} tags. use #{block_delimiter}"
else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag", :tag => tag))
raise SyntaxError, "Unknown tag '#{tag}'"
end
end
@@ -43,22 +71,21 @@ module Liquid
@tag_name
end
def create_variable(token)
token.scan(ContentOfVariable) 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 unterminated_variable(token)
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination", :token => token, :tag_end => VariableEnd.inspect))
end
def unterminated_tag(token)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination", :token => token, :tag_end => TagEnd.inspect))
end
def assert_missing_delimitation!
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed", :block_name => block_name))
raise SyntaxError.new("#{block_name} tag was never closed")
end
def render_all(list, context)
@@ -80,14 +107,12 @@ module Liquid
end
token_output = (token.respond_to?(:render) ? token.render(context) : token)
context.increment_used_resources(:render_length_current, token_output)
context.resource_limits[:render_length_current] += (token_output.respond_to?(:length) ? token_output.length : 1)
if context.resource_limits_reached?
context.resource_limits[:reached] = true
raise MemoryError.new("Memory limits exceeded")
end
unless token.is_a?(Block) && token.blank?
output << token_output
end
output << token_output
rescue MemoryError => e
raise e
rescue ::StandardError => e
@@ -95,7 +120,7 @@ module Liquid
end
end
StringSlice.join(output)
output.join
end
end
end

View File

@@ -25,15 +25,6 @@ module Liquid
squash_instance_assigns_with_environments
@interrupts = []
@filters = []
end
def increment_used_resources(key, obj)
@resource_limits[key] += if obj.kind_of?(StringSlice) || obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
obj.length
else
1
end
end
def resource_limits_reached?
@@ -43,7 +34,7 @@ module Liquid
end
def strainer
@strainer ||= Strainer.create(self, @filters)
@strainer ||= Strainer.create(self)
end
# Adds filters to this context.
@@ -52,20 +43,11 @@ module Liquid
# 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.add_known_filter(f)
end
# If strainer is already setup then there's no choice but to use a runtime
# extend call. If strainer is not yet created, we can utilize strainers
# cached class based API, which avoids busting the method cache.
if @strainer
filters.each do |f|
strainer.extend(f)
end
else
@filters.concat filters
strainer.extend(f)
end
end
@@ -171,15 +153,15 @@ module Liquid
LITERALS[key]
else
case key
when /\A'(.*)'\z/ # Single quoted strings
when /^'(.*)'$/ # Single quoted strings
$1
when /\A"(.*)"\z/ # Double quoted strings
when /^"(.*)"$/ # Double quoted strings
$1
when /\A(-?\d+)\z/ # Integer and floats
when /^(-?\d+)$/ # Integer and floats
$1.to_i
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
when /^\((\S+)\.\.(\S+)\)$/ # Ranges
(resolve($1).to_i..resolve($2).to_i)
when /\A(-?\d[\d\.]+)\z/ # Floats
when /^(-?\d[\d\.]+)$/ # Floats
$1.to_f
else
variable(key)
@@ -218,7 +200,7 @@ module Liquid
# assert_equal 'tobi', @context['hash["name"]']
def variable(markup)
parts = markup.scan(VariableParser)
square_bracketed = /\A\[(.*)\]\z/
square_bracketed = /^\[(.*)\]$/
first_part = parts.shift

View File

@@ -1,14 +1,13 @@
module Liquid
class Document < Block
# we don't need markup to open this block
def initialize(tokens, options = {})
@options = options
parse_body(tokens)
def initialize(tokens)
parse(tokens)
end
# There isn't a real delimiter
def block_delimiter
nil
[]
end
# Document blocks don't need to be terminated since they are not actually opened

View File

@@ -44,33 +44,17 @@ module Liquid
true
end
def inspect
self.class.to_s
end
def to_liquid
self
end
def to_s
self.class.name
end
alias :[] :invoke_drop
private
# Check for method existence without invoking respond_to?, which creates symbols
def self.invokable?(method_name)
unless @invokable_methods
blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable)
blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?]
end
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
@invokable_methods = Set.new(whitelist.map(&:to_s))
end
@invokable_methods ||= Set.new(["to_liquid"] + (public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
@invokable_methods.include?(method_name.to_s)
end
end

View File

@@ -31,22 +31,11 @@ module Liquid
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
#
# Optionally in the second argument you can specify a custom pattern for template filenames.
# The Kernel::sprintf format specification is used.
# Default pattern is "_%s.liquid".
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
#
# file_system.full_path("index") # => "/some/path/index.html"
#
class LocalFileSystem
attr_accessor :root
def initialize(root, pattern = "_%s.liquid")
def initialize(root)
@root = root
@pattern = pattern
end
def read_template_file(template_path, context)
@@ -57,15 +46,15 @@ module Liquid
end
def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
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), @pattern % File.basename(template_path))
File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
else
File.join(root, @pattern % template_path)
File.join(root, "_#{template_path}.liquid")
end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
full_path
end

View File

@@ -11,7 +11,7 @@ module Liquid
@attributes[key] = value
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row"))
raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
end
super
@@ -23,7 +23,7 @@ module Liquid
from = @attributes['offset'] ? context[@attributes['offset']].to_i : 0
to = @attributes['limit'] ? from + context[@attributes['limit']].to_i : nil
collection = Utils.slice_collection(collection, from, to)
collection = Utils.slice_collection_using_each(collection, from, to)
length = collection.length
@@ -55,7 +55,7 @@ module Liquid
col += 1
result << "<td class=\"col#{col}\">" << super << '</td>'
result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>'
if col == cols and (index != length - 1)
col = 0

View File

@@ -1,39 +0,0 @@
require 'yaml'
module Liquid
class I18n
DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
class TranslationError < StandardError
end
attr_reader :path
def initialize(path = DEFAULT_LOCALE)
@path = path
end
def translate(name, vars = {})
interpolate(deep_fetch_translation(name), vars)
end
alias_method :t, :translate
def locale
@locale ||= YAML.load_file(@path)
end
private
def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) {
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
"#{vars[$1.to_sym]}"
}
end
def deep_fetch_translation(name)
name.split('.').reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
end
end
end
end

View File

@@ -1,51 +0,0 @@
require "strscan"
module Liquid
class Lexer
SPECIALS = {
'|' => :pipe,
'.' => :dot,
':' => :colon,
',' => :comma,
'[' => :open_square,
']' => :close_square,
'(' => :open_round,
')' => :close_round
}
IDENTIFIER = /[\w\-?!]+/
SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
def initialize(input)
@ss = StringScanner.new(input.rstrip)
end
def tokenize
@output = []
while !@ss.eos?
@ss.skip(/\s*/)
tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
when t = @ss.scan(IDENTIFIER) then [:id, t]
when t = @ss.scan(DOTDOT) then [:dotdot, t]
else
c = @ss.getch
if s = SPECIALS[c]
[s,c]
else
raise SyntaxError, "Unexpected character #{c}"
end
end
@output << tok
end
@output << [:end_of_string]
end
end
end

View File

@@ -1,22 +0,0 @@
---
errors:
syntax:
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
for_invalid_in: "For loops require an 'in' clause"
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
unexpected_else: "%{block_name} tag does not expect else tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"

View File

@@ -1,89 +0,0 @@
module Liquid
class Parser
def initialize(input)
l = Lexer.new(input)
@tokens = l.tokenize
@p = 0 # pointer to current location
end
def jump(point)
@p = point
end
def consume(type = nil)
token = @tokens[@p]
if type && token[0] != type
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
end
@p += 1
token[1]
end
# Only consumes the token if it matches the type
# Returns the token's contents if it was consumed
# or false otherwise.
def consume?(type)
token = @tokens[@p]
return false unless token && token[0] == type
@p += 1
token[1]
end
# Like consume? Except for an :id token of a certain name
def id?(str)
token = @tokens[@p]
return false unless token && token[0] == :id
return false unless token[1] == str
@p += 1
token[1]
end
def look(type, ahead = 0)
tok = @tokens[@p + ahead]
return false unless tok
tok[0] == type
end
def expression
token = @tokens[@p]
if token[0] == :id
variable_signature
elsif [:string, :number].include? token[0]
consume
elsif token.first == :open_round
consume
first = expression
consume(:dotdot)
last = expression
consume(:close_round)
"(#{first}..#{last})"
else
raise SyntaxError, "#{token} is not a valid expression"
end
end
def argument
str = ""
# might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1)
str << consume << consume << ' '
end
str << expression
end
def variable_signature
str = consume(:id)
if look(:open_square)
str << consume
str << expression
str << consume(:close_square)
end
if look(:dot)
str << consume
str << variable_signature
end
str
end
end
end

View File

@@ -4,8 +4,6 @@ require 'bigdecimal'
module Liquid
module StandardFilters
HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
# Return the size of an array or of an string
def size(input)
@@ -33,7 +31,9 @@ module Liquid
end
def escape_once(input)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
ActionView::Helpers::TagHelper.escape_once(input)
rescue NameError
input
end
alias_method :h, :escape
@@ -43,7 +43,8 @@ module Liquid
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
truncated = RUBY_VERSION[0,3] == "1.8" ? input.scan(/./mu)[0...l].to_s : input[0...l]
input.length > length.to_i ? truncated + truncate_string : input
end
def truncatewords(input, words = 15, truncate_string = "...")
@@ -63,18 +64,6 @@ module Liquid
input.split(pattern)
end
def strip(input)
input.to_s.strip
end
def lstrip(input)
input.to_s.lstrip
end
def rstrip(input)
input.to_s.rstrip
end
def strip_html(input)
input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
end
@@ -92,7 +81,7 @@ module Liquid
# Sort elements of the array
# provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil)
ary = flatten_if_necessary(input)
ary = [input].flatten
if property.nil?
ary.sort
elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
@@ -110,8 +99,10 @@ module Liquid
# map/collect on a given property
def map(input, property)
flatten_if_necessary(input).map do |e|
ary = [input].flatten
ary.map do |e|
e = e.call if e.is_a?(Proc)
e = e.to_liquid if e.respond_to?(:to_liquid)
if property == "to_liquid"
e
@@ -190,7 +181,7 @@ module Liquid
return input.to_s
end
if ((input.is_a?(String) && !/\A\d+\z/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
if ((input.is_a?(String) && !/^\d+$/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
input = Time.at(input.to_i)
end
@@ -256,24 +247,8 @@ module Liquid
apply_operation(input, operand, :%)
end
def default(input, default_value = "")
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
is_blank ? default_value : input
end
private
def flatten_if_necessary(input)
ary = if input.is_a?(Array)
input.flatten
elsif input.is_a?(Enumerable) && !input.is_a?(Hash)
input
else
[input].flatten
end
ary.map{ |e| e.respond_to?(:to_liquid) ? e.to_liquid : e }
end
def to_number(obj)
case obj
when Float
@@ -281,7 +256,7 @@ module Liquid
when Numeric
obj
when String
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
(obj.strip =~ /^\d+\.\d+$/) ? BigDecimal.new(obj) : obj.to_i
else
0
end

View File

@@ -11,11 +11,6 @@ module Liquid
@@filters = []
@@known_filters = Set.new
@@known_methods = Set.new
@@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(Strainer) do
filters.each { |f| include f }
end
end
def initialize(context)
@context = context
@@ -37,13 +32,10 @@ module Liquid
end
end
def self.strainer_class_cache
@@strainer_class_cache
end
def self.create(context, filters = [])
filters = @@filters + filters
strainer_class_cache[filters].new(context)
def self.create(context)
strainer = Strainer.new(context)
@@filters.each { |m| strainer.extend(m) }
strainer
end
def invoke(method, *args)
@@ -52,8 +44,6 @@ module Liquid
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message)
end
def invokable?(method)

View File

@@ -1,21 +1,16 @@
module Liquid
class Tag
attr_accessor :options
attr_reader :nodelist, :warnings
def self.new_with_options(tag_name, markup, tokens, options)
# Forgive me Matz for I have sinned. I know this code is weird
# but it was necessary to maintain API compatibility.
new_tag = self.allocate
new_tag.options = options
new_tag.send(:initialize, tag_name, markup, tokens)
new_tag
end
class Tag
attr_accessor :nodelist
def initialize(tag_name, markup, tokens)
@tag_name = tag_name
@markup = markup
@options ||= {} # needs || because might be set before initialize
parse(tokens)
end
def parse(tokens)
end
def name
@@ -26,31 +21,6 @@ module Liquid
''
end
def blank?
@blank || false
end
def parse_with_selected_parser(markup)
case @options[:error_mode] || Template.error_mode
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
return strict_parse_with_error_context(markup)
rescue SyntaxError => e
@warnings ||= []
@warnings << e
return lax_parse(markup)
end
end
end
private
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.message << " in \"#{markup.strip}\""
raise e
end
end # Tag
end # Liquid

View File

@@ -16,7 +16,7 @@ module Liquid
@to = $1
@from = Variable.new($2)
else
raise SyntaxError.new options[:locale].t("errors.syntax.assign")
raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
end
super
@@ -25,13 +25,10 @@ module Liquid
def render(context)
val = @from.render(context)
context.scopes.last[@to] = val
context.increment_used_resources(:assign_score_current, val)
context.resource_limits[:assign_score_current] += (val.respond_to?(:length) ? val.length : 1)
''
end
def blank?
true
end
end
Template.register_tag('assign', Assign)

View File

@@ -18,7 +18,7 @@ module Liquid
if markup =~ Syntax
@to = $1
else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
end
super
@@ -27,13 +27,9 @@ module Liquid
def render(context)
output = super
context.scopes.last[@to] = output
context.increment_used_resources(:assign_score_current, output)
context.resource_limits[:assign_score_current] += (output.respond_to?(:length) ? output.length : 1)
''
end
def blank?
true
end
end
Template.register_tag('capture', Capture)

View File

@@ -9,16 +9,12 @@ module Liquid
if markup =~ Syntax
@left = $1
else
raise SyntaxError.new(options[:locale].t("errors.syntax.case"))
raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
end
super
end
def nodelist
@blocks.map(&:attachment).flatten
end
def unknown_tag(tag, markup, tokens)
@nodelist = []
case tag
@@ -54,7 +50,7 @@ module Liquid
while markup
# Create a new nodelist and assign it to the new block
if not markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when"))
raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ")
end
markup = $2
@@ -66,14 +62,17 @@ module Liquid
end
def record_else_condition(markup)
if not markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else"))
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)

View File

@@ -3,13 +3,6 @@ module Liquid
def render(context)
''
end
def unknown_tag(tag, markup, tokens)
end
def blank?
true
end
end
Template.register_tag('comment', Comment)

View File

@@ -1,4 +1,5 @@
module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
#
# {% for item in items %}
@@ -12,8 +13,8 @@ module Liquid
# <div class="green"> Item five</div>
#
class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/o
SimpleSyntax = /^#{QuotedFragment}+/o
NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/o
def initialize(tag_name, markup, tokens)
case markup
@@ -24,7 +25,7 @@ module Liquid
@variables = variables_from_string(markup)
@name = "'#{@variables.to_s}'"
else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle"))
raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]")
end
super
end
@@ -43,17 +44,15 @@ module Liquid
end
end
def blank?
false
end
private
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? $1 : nil
end.compact
end
end
Template.register_tag('cycle', Cycle)

View File

@@ -44,22 +44,26 @@ module Liquid
# forloop.last:: Returns true if the item is the last item.
#
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
Syntax = /\A(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
def initialize(tag_name, markup, tokens)
parse_with_selected_parser(markup)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@name = "#{$1}-#{$2}"
@reversed = $3
@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
@nodelist = @for_block = []
super
end
def nodelist
if @else_block
@for_block + @else_block
else
@for_block
end
end
def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'
@nodelist = @else_block = []
@@ -83,7 +87,8 @@ module Liquid
limit = context[@attributes['limit']]
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
segment = Utils.slice_collection_using_each(collection, from, to)
return render_else(context) if segment.empty?
@@ -122,43 +127,6 @@ module Liquid
result
end
protected
def lax_parse(markup)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@name = "#{$1}-#{$2}"
@reversed = $3
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.for"))
end
end
def strict_parse(markup)
p = Parser.new(markup)
@variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in")) unless p.id?('in')
@collection_name = p.expression
@name = "#{@variable_name}-#{@collection_name}"
@reversed = p.id?('reversed')
@attributes = {}
while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit') || p.id?('offset')
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute"))
end
p.consume
val = p.expression
@attributes[attribute] = val
end
p.consume(:end_of_string)
end
private
def render_else(context)

View File

@@ -1,4 +1,5 @@
module Liquid
# If is the conditional block
#
# {% if user.admin %}
@@ -9,19 +10,18 @@ module Liquid
#
# 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})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or)
def initialize(tag_name, markup, tokens)
@blocks = []
push_block('if', markup)
super
end
def nodelist
@blocks.map(&:attachment).flatten
push_block('if', markup)
super
end
def unknown_tag(tag, markup, tokens)
@@ -49,57 +49,30 @@ module Liquid
block = if tag == 'else'
ElseCondition.new
else
parse_with_selected_parser(markup)
expressions = markup.scan(ExpressionsAndOperators).reverse
raise(SyntaxError, SyntaxHelp) unless expressions.shift =~ Syntax
condition = Condition.new($1, $2, $3)
while not expressions.empty?
operator = (expressions.shift).to_s.strip
raise(SyntaxError, 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
def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators).reverse
raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift =~ Syntax
condition = Condition.new($1, $2, $3)
while not expressions.empty?
operator = (expressions.shift).to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift.to_s =~ Syntax
new_condition = Condition.new($1, $2, $3)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end
condition
end
def strict_parse(markup)
p = Parser.new(markup)
condition = parse_comparison(p)
while op = (p.id?('and') || p.id?('or'))
new_cond = parse_comparison(p)
new_cond.send(op, condition)
condition = new_cond
end
p.consume(:end_of_string)
condition
end
def parse_comparison(p)
a = p.expression
if op = p.consume?(:comparison)
b = p.expression
Condition.new(a, op, b)
else
Condition.new(a)
end
end
end
Template.register_tag('if', If)

View File

@@ -4,7 +4,7 @@ module Liquid
def render(context)
context.stack do
output = super
output = render_all(@nodelist, context)
if output != context.registers[:ifchanged]
context.registers[:ifchanged] = output

View File

@@ -29,14 +29,13 @@ module Liquid
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.include"))
raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]")
end
super
end
def blank?
false
def parse(tokens)
end
def render(context)
@@ -48,14 +47,13 @@ module Liquid
context[key] = context[value]
end
context_variable_name = @template_name[1..-2].split('/').last
if variable.is_a?(Array)
variable.collect do |var|
context[context_variable_name] = var
context[@template_name[1..-2]] = var
partial.render(context)
end
else
context[context_variable_name] = variable
context[@template_name[1..-2]] = variable
partial.render(context)
end
end

View File

@@ -1,11 +1,12 @@
module Liquid
# increment is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
# multiple instantiations of the template.
# (To achieve the survival, the application must keep the context)
#
# if the variable does not exist, it is created with value 0.
#
# Hello: {% increment variable %}
#
# gives you:
@@ -17,6 +18,7 @@ module Liquid
class Increment < Tag
def initialize(tag_name, markup, tokens)
@variable = markup.strip
super
end
@@ -26,9 +28,7 @@ module Liquid
value.to_s
end
def blank?
false
end
private
end
Template.register_tag('increment', Increment)

View File

@@ -1,14 +1,17 @@
module Liquid
class Raw < Block
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/o
FullTokenPossiblyInvalid = /^(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
def parse_body(tokens)
def parse(tokens)
@nodelist ||= []
@nodelist.clear
while token = tokens.shift
if token =~ FullTokenPossiblyInvalid
@nodelist << $1 if $1 != ""
return if block_delimiter == $2
if block_delimiter == $2
end_tag
return
end
end
@nodelist << token if not token.empty?
end

View File

@@ -14,10 +14,6 @@ module Liquid
# template.render('user_name' => 'bob')
#
class Template
DEFAULT_OPTIONS = {
:locale => I18n.new
}
attr_accessor :root, :resource_limits
@@file_system = BlankFileSystem.new
@@ -38,18 +34,6 @@ module Liquid
@tags ||= {}
end
# Sets how strict the parser should be.
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax.
def error_mode=(mode)
@error_mode = mode
end
def error_mode
@error_mode || :lax
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)
@@ -57,9 +41,9 @@ module Liquid
end
# creates a new <tt>Template</tt> object from liquid source code
def parse(source, options = {})
def parse(source)
template = Template.new
template.parse(source, options)
template.parse(source)
template
end
end
@@ -71,17 +55,11 @@ module Liquid
# Parse source code.
# Returns self for easy chaining
def parse(source, options = {})
@root = Document.new(tokenize(source), DEFAULT_OPTIONS.merge(options))
@warnings = nil
def parse(source)
@root = Document.new(tokenize(source))
self
end
def warnings
return [] unless @root
@warnings ||= @root.warnings
end
def registers
@registers ||= {}
end
@@ -123,7 +101,7 @@ module Liquid
when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
end
case args.last
@@ -162,9 +140,16 @@ module Liquid
private
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
def tokenize(source)
source = source.source if source.respond_to?(:source)
Tokenizer.new(source.to_s)
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

View File

@@ -1,18 +1,5 @@
module Liquid
module Utils
def self.slice_collection(collection, from, to)
if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
collection.load_slice(from, to)
else
slice_collection_using_each(collection, from, to)
end
end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''
end
def self.slice_collection_using_each(collection, from, to)
segments = []
index = 0
@@ -35,5 +22,9 @@ module Liquid
segments
end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''
end
end
end

View File

@@ -12,30 +12,11 @@ module Liquid
#
class Variable
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
attr_accessor :filters, :name, :warnings
attr_accessor :filters, :name
def initialize(markup, options = {})
def initialize(markup)
@markup = markup
@name = nil
@options = options || {}
case @options[:error_mode] || Template.error_mode
when :strict then strict_parse(markup)
when :lax then lax_parse(markup)
when :warn
begin
strict_parse(markup)
rescue SyntaxError => e
@warnings ||= []
@warnings << e
lax_parse(markup)
end
end
end
def lax_parse(markup)
@filters = []
if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
@name = match[1]
@@ -52,39 +33,6 @@ module Liquid
end
end
def strict_parse(markup)
# Very simple valid cases
if markup =~ EasyParse
@name = $1
@filters = []
return
end
@filters = []
p = Parser.new(markup)
# Could be just filters with no input
@name = p.look(:pipe) ? '' : p.expression
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@filters << [filtername, filterargs]
end
p.consume(:end_of_string)
rescue SyntaxError => e
e.message << " in \"{{#{markup}}}\""
raise e
end
def parse_filterargs(p)
# first argument
filterargs = [p.argument]
# followed by comma separated others
while p.consume?(:comma)
filterargs << p.argument
end
filterargs
end
def render(context)
return '' if @name.nil?
@filters.inject(context[@name]) do |output, filter|

View File

@@ -1,4 +1,4 @@
# encoding: utf-8
module Liquid
VERSION = "3.0.0"
VERSION = "2.6.0.rc1"
end

View File

@@ -12,21 +12,14 @@ Gem::Specification.new do |s|
s.authors = ["Tobias Luetke"]
s.email = ["tobi@leetsoft.com"]
s.homepage = "http://www.liquidmarkup.org"
s.license = "MIT"
#s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib,ext}/**/*") + %w(MIT-LICENSE README.md)
s.extensions = ['ext/liquid/extconf.rb']
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib"
s.add_development_dependency 'rake-compiler'
s.add_development_dependency 'stackprof'
s.add_development_dependency 'rake'
s.add_development_dependency 'activesupport'
end

View File

@@ -2,7 +2,6 @@ require 'rubygems'
require 'benchmark'
require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.bmbm do |x|

View File

@@ -6,14 +6,14 @@ profiler = ThemeRunner.new
puts 'Running profiler...'
results = profiler.run_profile
results = profiler.run
puts 'Success'
puts
[RubyProf::FlatPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter, RubyProf::DotPrinter].each do |klass|
[RubyProf::FlatPrinter, RubyProf::GraphPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter].each do |klass|
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/callgrind.liquid.#{klass.name.downcase}.txt")
filename.gsub!(/:+/, '_')
File.open(filename, "w+") { |fp| klass.new(results).print(fp, :print_file => true, :min_percent => 3) }
File.open(filename, "w+") { |fp| klass.new(results).print(fp, :print_file => true) }
$stderr.puts "wrote #{klass.name} output to #{filename}"
end

View File

@@ -54,7 +54,7 @@ module ShopFilter
def product_img_url(url, style = 'small')
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
unless url =~ /^products\/([\w\-\_]+)\.(\w{2,4})/
raise ArgumentError, 'filter "size" can only be called on product images'
end

View File

@@ -1,15 +0,0 @@
require 'stackprof' rescue fail("install stackprof extension/gem")
require File.dirname(__FILE__) + '/theme_runner'
profiler = ThemeRunner.new
profiler.run
results = StackProf.run(mode: :cpu, out: ENV['FILENAME']) do
100.times do
profiler.run
end
end
if results.kind_of?(File)
puts "wrote stackprof dump to #{results.path}"
else
StackProf::Report.new(results).print_text(false, 20)
end

View File

@@ -28,7 +28,7 @@
{% else %}
<div class="article-body textile">
In <em>Admin &gt; Blogs &amp; Pages</em>, create a page with the handle <strong><code>frontpage</code></strong> and it will show up here.<br />
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
{{ "Learn more about handles" | link_to "http://wiki.shopify.com/Handle" }}
</div>
{% endif %}

View File

@@ -26,7 +26,7 @@
{{ article.content }}
{% else %}
In <em>Admin &gt; Blogs &amp; Pages</em>, create a page with the handle <strong><code>frontpage</code></strong> and it will show up here.<br />
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
{{ "Learn more about handles" | link_to "http://wiki.shopify.com/Handle" }}
{% endif %}
</div>
</div>

View File

@@ -5,7 +5,7 @@
{{ article.content }}
{% else %}
In <em>Admin &gt; Blogs &amp; Pages</em>, create a page with the handle <strong><code>frontpage</code></strong> and it will show up here.<br />
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
{{ "Learn more about handles" | link_to "http://wiki.shopify.com/Handle" }}
{% endif %}
</div>

View File

@@ -1,9 +0,0 @@
---
simple: "less is more"
whatever: "something %{something}"
errors:
i18n:
undefined_interpolation: "undefined key %{key}"
unknown_translation: "translation '%{name}' wasn't found"
syntax:
oops: "something wasn't right"

View File

@@ -18,10 +18,4 @@ class AssignTest < Test::Unit::TestCase
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => "foo,bar,baz")
end
def test_assign_syntax_error
assert_match_syntax_error(/assign/,
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
end
end # AssignTest

View File

@@ -1,106 +0,0 @@
require 'test_helper'
class FoobarTag < Liquid::Tag
def render(*args)
" "
end
Liquid::Template.register_tag('foobar', FoobarTag)
end
class BlankTestFileSystem
def read_template_file(template_path, context)
template_path
end
end
class BlankTest < Test::Unit::TestCase
include Liquid
N = 10
def wrap_in_for(body)
"{% for i in (1..#{N}) %}#{body}{% endfor %}"
end
def wrap_in_if(body)
"{% if true %}#{body}{% endif %}"
end
def wrap(body)
wrap_in_for(body) + wrap_in_if(body)
end
def test_new_tags_are_not_blank_by_default
assert_template_result(" "*N, wrap_in_for("{% foobar %}"))
end
def test_loops_are_blank
assert_template_result("", wrap_in_for(" "))
end
def test_if_else_are_blank
assert_template_result("", "{% if true %} {% elsif false %} {% else %} {% endif %}")
end
def test_unless_is_blank
assert_template_result("", wrap("{% unless true %} {% endunless %}"))
end
def test_mark_as_blank_only_during_parsing
assert_template_result(" "*(N+1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
end
def test_comments_are_blank
assert_template_result("", wrap(" {% comment %} whatever {% endcomment %} "))
end
def test_captures_are_blank
assert_template_result("", wrap(" {% capture foo %} whatever {% endcapture %} "))
end
def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" ")))
assert_template_result("\n but this is not "*(N+1),
wrap(%q{{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}}))
end
def test_assigns_are_blank
assert_template_result("", wrap(' {% assign foo = "bar" %} '))
end
def test_whitespace_is_blank
assert_template_result("", wrap(" "))
assert_template_result("", wrap("\t"))
end
def test_whitespace_is_not_blank_if_other_stuff_is_present
body = " x "
assert_template_result(body*(N+1), wrap(body))
end
def test_increment_is_not_blank
assert_template_result(" 0"*2*(N+1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
end
def test_cycle_is_not_blank
assert_template_result(" "*((N+1)/2)+" ", wrap("{% cycle ' ', ' ' %}"))
end
def test_raw_is_not_blank
assert_template_result(" "*(N+1), wrap(" {% raw %} {% endraw %}"))
end
def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new
assert_equal "foobar"*(N+1), Template.parse(wrap("{% include 'foobar' %}")).render()
assert_equal " foobar "*(N+1), Template.parse(wrap("{% include ' foobar ' %}")).render()
assert_equal " ", Template.parse(" {% include ' ' %} ").render()
end
def test_case_is_blank
assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result(" x "*(N+1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
end
end

View File

@@ -12,34 +12,34 @@ class BlockTest < Test::Unit::TestCase
template = Liquid::Template.parse("{{funk}} ")
assert_equal 2, template.root.nodelist.size
assert_equal Variable, template.root.nodelist[0].class
assert_equal StringSlice, template.root.nodelist[1].class
assert_equal String, template.root.nodelist[1].class
end
def test_variable_end
template = Liquid::Template.parse(" {{funk}}")
assert_equal 2, template.root.nodelist.size
assert_equal StringSlice, template.root.nodelist[0].class
assert_equal String, template.root.nodelist[0].class
assert_equal Variable, template.root.nodelist[1].class
end
def test_variable_middle
template = Liquid::Template.parse(" {{funk}} ")
assert_equal 3, template.root.nodelist.size
assert_equal StringSlice, template.root.nodelist[0].class
assert_equal String, template.root.nodelist[0].class
assert_equal Variable, template.root.nodelist[1].class
assert_equal StringSlice, template.root.nodelist[2].class
assert_equal String, template.root.nodelist[2].class
end
def test_variable_many_embedded_fragments
template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
assert_equal 7, template.root.nodelist.size
assert_equal [StringSlice, Variable, StringSlice, Variable, StringSlice, Variable, StringSlice],
assert_equal [String, Variable, String, Variable, String, Variable, String],
block_types(template.root.nodelist)
end
def test_with_block
template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
assert_equal [StringSlice, Comment, StringSlice], block_types(template.root.nodelist)
assert_equal [String, Comment, String], block_types(template.root.nodelist)
assert_equal 3, template.root.nodelist.size
end

View File

@@ -55,44 +55,11 @@ class ProductDrop < Liquid::Drop
end
class EnumerableDrop < Liquid::Drop
def before_method(method)
method
end
def size
3
end
def first
1
end
def count
3
end
def min
1
end
def max
3
end
def each
yield 1
yield 2
yield 3
end
end
class RealEnumerableDrop < Liquid::Drop
include Enumerable
def before_method(method)
method
end
def each
yield 1
yield 2
@@ -203,33 +170,6 @@ class DropsTest < Test::Unit::TestCase
assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render('collection' => EnumerableDrop.new)
end
def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new)
end
end
def test_some_enumerable_methods_still_get_invoked
[ :count, :max ].each do |method|
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new)
end
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render('collection' => RealEnumerableDrop.new)
[ :min, :first ].each do |method|
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render('collection' => EnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render('collection' => EnumerableDrop.new)
end
end
def test_empty_string_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => '')
end
@@ -237,9 +177,4 @@ class DropsTest < Test::Unit::TestCase
def test_nil_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => nil)
end
def test_default_to_s_on_drops
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render('product' => ProductDrop.new)
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render('collection' => EnumerableDrop.new)
end
end # DropsTest

View File

@@ -58,48 +58,19 @@ class ErrorHandlingTest < Test::Unit::TestCase
def test_missing_endtag_parse_time_error
assert_raise(Liquid::SyntaxError) do
Liquid::Template.parse(' {% for a in b %} ... ')
template = Liquid::Template.parse(' {% for a in b %} ... ')
end
end
def test_unrecognized_operator
with_error_mode(:strict) do
assert_raise(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
end
end
def test_lax_unrecognized_operator
assert_nothing_raised do
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :lax)
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
assert_equal ' Liquid error: Unknown operator =! ', template.render
assert_equal 1, template.errors.size
assert_equal Liquid::ArgumentError, template.errors.first.class
end
end
def test_strict_error_messages
err = assert_raise(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', :error_mode => :strict)
end
assert_equal 'Unexpected character = in "1 =! 2"', err.message
err = assert_raise(SyntaxError) do
Liquid::Template.parse('{{%%%}}', :error_mode => :strict)
end
assert_equal 'Unexpected character % in "{{%%%}}"', err.message
end
def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', :error_mode => :warn)
assert_equal 3, template.warnings.size
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].message
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].message
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
assert_equal '', template.render
end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate
assert_raise Exception do

View File

@@ -26,10 +26,4 @@ class FileSystemTest < Test::Unit::TestCase
file_system.full_path("/etc/passwd")
end
end
def test_custom_template_filename_patterns
file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
assert_equal "/some/path/mypartial.html" , file_system.full_path("mypartial")
assert_equal "/some/path/dir/mypartial.html", file_system.full_path("dir/mypartial")
end
end # FileSystemTest

View File

@@ -1,37 +0,0 @@
require 'test_helper'
class I18nTest < Test::Unit::TestCase
include Liquid
def setup
@i18n = I18n.new(fixture("en_locale.yml"))
end
def test_simple_translate_string
assert_equal "less is more", @i18n.translate("simple")
end
def test_nested_translate_string
assert_equal "something wasn't right", @i18n.translate("errors.syntax.oops")
end
def test_single_string_interpolation
assert_equal "something different", @i18n.translate("whatever", :something => "different")
end
# def test_raises_translation_error_on_undefined_interpolation_key
# assert_raise I18n::TranslationError do
# @i18n.translate("whatever", :oopstypos => "yes")
# end
# end
def test_raises_unknown_translation
assert_raise I18n::TranslationError do
@i18n.translate("doesnt_exist")
end
end
def test_sets_default_path_to_en
assert_equal I18n::DEFAULT_LOCALE, I18n.new.path
end
end

View File

@@ -1,48 +0,0 @@
require 'test_helper'
class LexerTest < Test::Unit::TestCase
include Liquid
def test_strings
tokens = Lexer.new(%! 'this is a test""' "wat 'lol'"!).tokenize
assert_equal [[:string,%!'this is a test""'!], [:string, %!"wat 'lol'"!], [:end_of_string]], tokens
end
def test_integer
tokens = Lexer.new('hi 50').tokenize
assert_equal [[:id,'hi'], [:number, '50'], [:end_of_string]], tokens
end
def test_float
tokens = Lexer.new('hi 5.0').tokenize
assert_equal [[:id,'hi'], [:number, '5.0'], [:end_of_string]], tokens
end
def test_comparison
tokens = Lexer.new('== <> contains').tokenize
assert_equal [[:comparison,'=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens
end
def test_specials
tokens = Lexer.new('| .:').tokenize
assert_equal [[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]], tokens
tokens = Lexer.new('[,]').tokenize
assert_equal [[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]], tokens
end
def test_fancy_identifiers
tokens = Lexer.new('hi! five?').tokenize
assert_equal [[:id,'hi!'], [:id, 'five?'], [:end_of_string]], tokens
end
def test_whitespace
tokens = Lexer.new("five|\n\t ==").tokenize
assert_equal [[:id,'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]], tokens
end
def test_unexpected_character
assert_raises(SyntaxError) do
Lexer.new("%").tokenize
end
end
end

View File

@@ -86,7 +86,7 @@ class OutputTest < Test::Unit::TestCase
assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end
def test_variable_piping_with_multiple_args
def test_variable_piping_with_args
text = %! {{ car.gm | add_tag : 'span', 'bar'}} !
expected = %| <span id="bar">bad</span> |

View File

@@ -1,82 +0,0 @@
require 'test_helper'
class ParserTest < Test::Unit::TestCase
include Liquid
def test_consume
p = Parser.new("wat: 7")
assert_equal 'wat', p.consume(:id)
assert_equal ':', p.consume(:colon)
assert_equal '7', p.consume(:number)
end
def test_jump
p = Parser.new("wat: 7")
p.jump(2)
assert_equal '7', p.consume(:number)
end
def test_consume?
p = Parser.new("wat: 7")
assert_equal 'wat', p.consume?(:id)
assert_equal false, p.consume?(:dot)
assert_equal ':', p.consume(:colon)
assert_equal '7', p.consume?(:number)
end
def test_id?
p = Parser.new("wat 6 Peter Hegemon")
assert_equal 'wat', p.id?('wat')
assert_equal false, p.id?('endgame')
assert_equal '6', p.consume(:number)
assert_equal 'Peter', p.id?('Peter')
assert_equal false, p.id?('Achilles')
end
def test_look
p = Parser.new("wat 6 Peter Hegemon")
assert_equal true, p.look(:id)
assert_equal 'wat', p.consume(:id)
assert_equal false, p.look(:comparison)
assert_equal true, p.look(:number)
assert_equal true, p.look(:id, 1)
assert_equal false, p.look(:number, 1)
end
def test_expressions
p = Parser.new("hi.there hi[5].! hi.there.bob")
assert_equal 'hi.there', p.expression
assert_equal 'hi[5].!', p.expression
assert_equal 'hi.there.bob', p.expression
p = Parser.new("567 6.0 'lol' \"wut\"")
assert_equal '567', p.expression
assert_equal '6.0', p.expression
assert_equal "'lol'", p.expression
assert_equal '"wut"', p.expression
end
def test_ranges
p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
assert_equal '(5..7)', p.expression
assert_equal '(1.5..9.6)', p.expression
assert_equal '(young..old)', p.expression
assert_equal '(hi[5].wat..old)', p.expression
end
def test_arguments
p = Parser.new("filter: hi.there[5], keyarg: 7")
assert_equal 'filter', p.consume(:id)
assert_equal ':', p.consume(:colon)
assert_equal 'hi.there[5]', p.argument
assert_equal ',', p.consume(:comma)
assert_equal 'keyarg: 7', p.argument
end
def test_invalid_expression
assert_raises(SyntaxError) do
p = Parser.new("==")
p.expression
end
end
end

View File

@@ -8,7 +8,7 @@ class ParsingQuirksTest < Test::Unit::TestCase
template = Template.parse(text)
assert_equal text, template.render
assert_equal [StringSlice], template.root.nodelist.collect {|i| i.class}
assert_equal [String], template.root.nodelist.collect {|i| i.class}
end
def test_raise_on_single_close_bracet
@@ -31,60 +31,22 @@ class ParsingQuirksTest < Test::Unit::TestCase
def test_error_on_empty_filter
assert_nothing_raised do
Template.parse("{{test |a|b|}}")
Template.parse("{{test}}")
Template.parse("{{|test}}")
end
with_error_mode(:strict) do
assert_raise(SyntaxError) do
Template.parse("{{test |a|b|}}")
end
Template.parse("{{|test|}}")
end
end
def test_meaningless_parens_error
with_error_mode(:strict) do
assert_raise(SyntaxError) do
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
end
def test_meaningless_parens
assigns = {'b' => 'bar', 'c' => 'baz'}
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns)
end
def test_unexpected_characters_syntax_error
with_error_mode(:strict) do
assert_raise(SyntaxError) do
markup = "true && false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
assert_raise(SyntaxError) do
markup = "false || true"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
end
end
def test_no_error_on_lax_empty_filter
assert_nothing_raised do
Template.parse("{{test |a|b|}}", :error_mode => :lax)
Template.parse("{{test}}", :error_mode => :lax)
Template.parse("{{|test|}}", :error_mode => :lax)
end
end
def test_meaningless_parens_lax
with_error_mode(:lax) do
assigns = {'b' => 'bar', 'c' => 'baz'}
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}", assigns)
end
end
def test_unexpected_characters_silently_eat_logic_lax
with_error_mode(:lax) do
markup = "true && false"
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}")
markup = "false || true"
assert_template_result('',"{% if #{markup} %} YES {% endif %}")
end
def test_unexpected_characters_silently_eat_logic
markup = "true && false"
assert_template_result(' YES ',"{% if #{markup} %} YES {% endif %}")
markup = "false || true"
assert_template_result('',"{% if #{markup} %} YES {% endif %}")
end
end # ParsingQuirksTest

View File

@@ -21,11 +21,11 @@ class RegexpTest < Test::Unit::TestCase
assert_equal ['<style', 'class="hello">', '</style>'], %|<style class="hello">' </style>|.scan(QuotedFragment)
end
def test_double_quoted_words
def test_quoted_words
assert_equal ['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment)
end
def test_single_quoted_words
def test_quoted_words
assert_equal ['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment)
end

View File

@@ -15,10 +15,6 @@ class TestThing
"woot: #{@foo}"
end
def [](whatever)
to_s
end
def to_liquid
@foo += 1
self
@@ -31,14 +27,6 @@ class TestDrop < Liquid::Drop
end
end
class TestEnumerable < Liquid::Drop
include Enumerable
def each(&block)
[ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block)
end
end
class StandardFiltersTest < Test::Unit::TestCase
include Liquid
@@ -62,6 +50,11 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal '', @filters.upcase(nil)
end
def test_upcase
assert_equal 'TESTING', @filters.upcase("Testing")
assert_equal '', @filters.upcase(nil)
end
def test_truncate
assert_equal '1234...', @filters.truncate('1234567890', 7)
assert_equal '1234567890', @filters.truncate('1234567890', 20)
@@ -70,7 +63,7 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
end
def test_split
def test_strip
assert_equal ['12','34'], @filters.split('12~34', '~')
assert_equal ['A? ',' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
assert_equal ['A?Z'], @filters.split('A?Z', '~')
@@ -84,7 +77,7 @@ class StandardFiltersTest < Test::Unit::TestCase
end
def test_escape_once
assert_equal '&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>')
assert_equal '&lt;strong&gt;', @filters.escape_once(@filters.escape('<strong>'))
end
def test_truncatewords
@@ -132,16 +125,7 @@ class StandardFiltersTest < Test::Unit::TestCase
def test_map_calls_to_liquid
t = TestThing.new
assert_equal "woot: 1", Liquid::Template.parse('{{ foo | map: "whatever" }}').render("foo" => [t])
end
def test_map_on_hashes
assert_equal "4217", Liquid::Template.parse('{{ thing | map: "foo" | map: "bar" }}').render("thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] })
end
def test_sort_calls_to_liquid
t = TestThing.new
assert_equal "woot: 1", Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
assert_equal "woot: 1", Liquid::Template.parse('{{ foo }}').render("foo" => t)
end
def test_map_over_proc
@@ -151,14 +135,6 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_equal "testfoo", Liquid::Template.parse(templ).render("procs" => [p])
end
def test_map_works_on_enumerables
assert_equal "123", Liquid::Template.parse('{{ foo | map: "foo" }}').render!("foo" => TestEnumerable.new)
end
def test_sort_works_on_enumerables
assert_equal "213", Liquid::Template.parse('{{ foo | sort: "bar" | map: "foo" }}').render!("foo" => TestEnumerable.new)
end
def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
@@ -209,21 +185,6 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result 'foobar', "{{ 'foo|bar' | remove: '|' }}"
end
def test_strip
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " ab c "
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " \tab c \n \t"
end
def test_lstrip
assert_template_result 'ab c ', "{{ source | lstrip }}", 'source' => " ab c "
assert_template_result "ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t"
end
def test_rstrip
assert_template_result " ab c", "{{ source | rstrip }}", 'source' => " ab c "
assert_template_result " \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t"
end
def test_strip_newlines
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc"
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc"
@@ -247,6 +208,9 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result "12", "{{ 3 | times:4 }}"
assert_template_result "0", "{{ 'foo' | times:4 }}"
# Ruby v1.9.2-rc1, or higher, backwards compatible Float test
assert_match(/(6\.3)|(6\.(0{13})1)/, Template.parse("{{ '2.1' | times:3 }}").render)
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
@@ -256,6 +220,9 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result "4", "{{ 12 | divided_by:3 }}"
assert_template_result "4", "{{ 14 | divided_by:3 }}"
# Ruby v1.9.2-rc1, or higher, backwards compatible Float test
assert_match(/4\.(6{13,14})7/, Template.parse("{{ 14 | divided_by:'3.0' }}").render)
assert_template_result "5", "{{ 15 | divided_by:3 }}"
assert_template_result "Liquid error: divided by 0", "{{ 5 | divided_by:0 }}"
@@ -278,15 +245,6 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result('abc',"{{ a | prepend: b}}",assigns)
end
def test_default
assert_equal "foo", @filters.default("foo", "bar")
assert_equal "bar", @filters.default(nil, "bar")
assert_equal "bar", @filters.default("", "bar")
assert_equal "bar", @filters.default(false, "bar")
assert_equal "bar", @filters.default([], "bar")
assert_equal "bar", @filters.default({}, "bar")
end
def test_cannot_access_private_methods
assert_template_result('a',"{{ 'a' | to_number }}")
end

View File

@@ -22,13 +22,6 @@ class StrainerTest < Test::Unit::TestCase
assert_equal "public", strainer.invoke("public_filter")
end
def test_stainer_raises_argument_error
strainer = Strainer.create(nil)
assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
end
end
def test_strainer_only_invokes_public_filter_methods
strainer = Strainer.create(nil)
assert_equal false, strainer.invokable?('__test__')
@@ -56,15 +49,4 @@ class StrainerTest < Test::Unit::TestCase
assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke")
end
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a, b = Module.new, Module.new
strainer = Strainer.create(nil, [a,b])
assert_kind_of Strainer, strainer
assert_kind_of a, strainer
assert_kind_of b, strainer
Strainer.class_variable_get(:@@filters).each do |m|
assert_kind_of m, strainer
end
end
end # StrainerTest

View File

@@ -1,34 +0,0 @@
require 'test_helper'
class StringSliceTest < Test::Unit::TestCase
def test_new_from_string
assert_equal 'slice', Liquid::StringSlice.new("slice and dice", 0, 5).to_str
assert_equal 'and', Liquid::StringSlice.new("slice and dice", 6, 3).to_str
assert_equal 'dice', Liquid::StringSlice.new("slice and dice", 10, 4).to_str
assert_equal 'slice and dice', Liquid::StringSlice.new("slice and dice", 0, 14).to_str
end
def test_new_from_slice
slice1 = Liquid::StringSlice.new("slice and dice", 0, 14)
slice2 = Liquid::StringSlice.new(slice1, 6, 8)
slice3 = Liquid::StringSlice.new(slice2, 0, 3)
assert_equal "slice and dice", slice1.to_str
assert_equal "and dice", slice2.to_str
assert_equal "and", slice3.to_str
end
def test_slice
slice = Liquid::StringSlice.new("slice and dice", 2, 10)
assert_equal "and", slice.slice(4, 3).to_str
end
def test_length
slice = Liquid::StringSlice.new("slice and dice", 6, 3)
assert_equal 3, slice.length
assert_equal 3, slice.size
end
def test_equal
assert_equal 'and', Liquid::StringSlice.new("slice and dice", 6, 3)
end
end

View File

@@ -1,10 +0,0 @@
require 'test_helper'
class CaseTagTest < Test::Unit::TestCase
include Liquid
def test_case_nodelist
template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}')
assert_equal ['WHEN', 'ELSE'], template.root.nodelist[0].nodelist
end
end # CaseTest

View File

@@ -294,72 +294,4 @@ HERE
assigns = {'items' => [1,2,3,4,5]}
assert_template_result(expected, template, assigns)
end
def test_for_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}')
assert_equal ['FOR'], template.root.nodelist[0].nodelist
end
def test_for_else_nodelist
template = Liquid::Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}')
assert_equal ['FOR', 'ELSE'], template.root.nodelist[0].nodelist
end
class LoaderDrop < Liquid::Drop
attr_accessor :each_called, :load_slice_called
def initialize(data)
@data = data
end
def each
@each_called = true
@data.each { |el| yield el }
end
def load_slice(from, to)
@load_slice_called = true
@data[(from..to-1)]
end
end
def test_iterate_with_each_when_no_limit_applied
loader = LoaderDrop.new([1,2,3,4,5])
assigns = {'items' => loader}
expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns)
assert loader.each_called
assert !loader.load_slice_called
end
def test_iterate_with_load_slice_when_limit_applied
loader = LoaderDrop.new([1,2,3,4,5])
assigns = {'items' => loader}
expected = '1'
template = '{% for item in items limit:1 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns)
assert !loader.each_called
assert loader.load_slice_called
end
def test_iterate_with_load_slice_when_limit_and_offset_applied
loader = LoaderDrop.new([1,2,3,4,5])
assigns = {'items' => loader}
expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns)
assert !loader.each_called
assert loader.load_slice_called
end
def test_iterate_with_load_slice_returns_same_results_as_without
loader = LoaderDrop.new([1,2,3,4,5])
loader_assigns = {'items' => loader}
array_assigns = {'items' => [1,2,3,4,5]}
expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, loader_assigns)
assert_template_result(expected, template, array_assigns)
end
end

View File

@@ -157,15 +157,4 @@ class IfElseTagTest < Test::Unit::TestCase
assert_template_result('yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}))
end
def test_if_nodelist
template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}')
assert_equal ['IF', 'ELSE'], template.root.nodelist[0].nodelist
end
def test_operators_are_whitelisted
assert_raise(SyntaxError) do
assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %}))
end
end
end # IfElseTest

View File

@@ -48,27 +48,6 @@ class CountingFileSystem
end
end
class CustomInclude < Liquid::Tag
Syntax = /(#{Liquid::QuotedFragment}+)(\s+(?:with|for)\s+(#{Liquid::QuotedFragment}+))?/o
def initialize(tag_name, markup, tokens)
markup =~ Syntax
@template_name = $1
super
end
def parse(tokens)
end
def blank?
false
end
def render(context)
@template_name[1..-2]
end
end
class IncludeTagTest < Test::Unit::TestCase
include Liquid
@@ -184,31 +163,4 @@ class IncludeTagTest < Test::Unit::TestCase
Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => file_system})
assert_equal 2, file_system.count
end
def test_include_tag_within_if_statement
assert_equal "foo_if_true",
Template.parse("{% if true %}{% include 'foo_if_true' %}{% endif %}").render
end
def test_custom_include_tag
original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude
begin
assert_equal "custom_foo",
Template.parse("{% include 'custom_foo' %}").render
ensure
Liquid::Template.tags['include'] = original_tag
end
end
def test_custom_include_tag_within_if_statement
original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude
begin
assert_equal "custom_foo_if_true",
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render
ensure
Liquid::Template.tags['include'] = original_tag
end
end
end # IncludeTagTest

View File

@@ -33,13 +33,6 @@ class StandardTagTest < Test::Unit::TestCase
assert_template_result('','{% comment %}{% endcomment %}')
assert_template_result('','{%comment%}comment{%endcomment%}')
assert_template_result('','{% comment %}comment{% endcomment %}')
assert_template_result('','{% comment %} 1 {% comment %} 2 {% endcomment %} 3 {% endcomment %}')
assert_template_result('','{%comment%}{%blabla%}{%endcomment%}')
assert_template_result('','{% comment %}{% blabla %}{% endcomment %}')
assert_template_result('','{%comment%}{% endif %}{%endcomment%}')
assert_template_result('','{% comment %}{% endwhatever %}{% endcomment %}')
assert_template_result('','{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
assert_template_result('foobar','foo{%comment%}comment{%endcomment%}bar')
assert_template_result('foobar','foo{% comment %}comment{% endcomment %}bar')
@@ -54,9 +47,16 @@ class StandardTagTest < Test::Unit::TestCase
{%endcomment%}bar')
end
def test_assign
assigns = {'var' => 'content' }
assert_template_result('var2: var2:content', 'var2:{{var2}} {%assign var2 = var%} var2:{{var2}}', assigns)
end
def test_hyphenated_assign
assigns = {'a-b' => '1' }
assert_template_result('a-b:1 a-b:2', 'a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}', assigns)
end
def test_assign_with_colon_and_spaces
@@ -218,12 +218,7 @@ class StandardTagTest < Test::Unit::TestCase
end
def test_assign
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}').render
end
def test_assign_unassigned
assigns = { 'var' => 'content' }
assert_template_result('var2: var2:content', 'var2:{{var2}} {%assign var2 = var%} var2:{{var2}}', assigns)
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render
end
def test_assign_an_empty_string

View File

@@ -14,17 +14,29 @@ class TemplateContextDrop < Liquid::Drop
end
end
class SomethingWithLength
def length
nil
end
liquid_methods :length
end
class TemplateTest < Test::Unit::TestCase
include Liquid
def test_tokenize_strings
assert_equal [' '], Template.new.send(:tokenize, ' ')
assert_equal ['hello world'], Template.new.send(:tokenize, 'hello world')
end
def test_tokenize_variables
assert_equal ['{{funk}}'], Template.new.send(:tokenize, '{{funk}}')
assert_equal [' ', '{{funk}}', ' '], Template.new.send(:tokenize, ' {{funk}} ')
assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], Template.new.send(:tokenize, ' {{funk}} {{so}} {{brother}} ')
assert_equal [' ', '{{ funk }}', ' '], Template.new.send(:tokenize, ' {{ funk }} ')
end
def test_tokenize_blocks
assert_equal ['{%comment%}'], Template.new.send(:tokenize, '{%comment%}')
assert_equal [' ', '{%comment%}', ' '], Template.new.send(:tokenize, ' {%comment%} ')
assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], Template.new.send(:tokenize, ' {%comment%} {%endcomment%} ')
assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], Template.new.send(:tokenize, " {% comment %} {% endcomment %} ")
end
def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render
@@ -74,12 +86,6 @@ class TemplateTest < Test::Unit::TestCase
@global = nil
end
def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}")
t.resource_limits = { :render_length_limit => 42 }
assert_equal "", t.render("bar" => SomethingWithLength.new)
end
def test_resource_limits_render_length
t = Template.parse("0123456789")
t.resource_limits = { :render_length_limit => 5 }
@@ -137,18 +143,4 @@ class TemplateTest < Test::Unit::TestCase
assert_equal 'bar', t.parse('{{bar}}').render(drop)
assert_equal 'haha', t.parse("{{baz}}").render(drop)
end
def test_sets_default_localization_in_document
t = Template.new
t.parse('')
assert_instance_of I18n, t.root.options[:locale]
end
def test_sets_default_localization_in_context_with_quick_initialization
t = Template.new
t.parse('{{foo}}', :locale => I18n.new(fixture("en_locale.yml")))
assert_instance_of I18n, t.root.options[:locale]
assert_equal fixture("en_locale.yml"), t.root.options[:locale].path
end
end
end # TemplateTest

View File

@@ -1,64 +0,0 @@
require 'test_helper'
class TokenizerTest < Test::Unit::TestCase
def test_tokenize_strings
assert_equal [' '], tokenize(' ')
assert_equal ['hello world'], tokenize('hello world')
end
def test_tokenize_variables
assert_equal ['{{funk}}'], tokenize('{{funk}}')
assert_equal [' ', '{{funk}}', ' '], tokenize(' {{funk}} ')
assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], tokenize(' {{funk}} {{so}} {{brother}} ')
assert_equal [' ', '{{ funk }}', ' '], tokenize(' {{ funk }} ')
end
def test_tokenize_blocks
assert_equal ['{%comment%}'], tokenize('{%comment%}')
assert_equal [' ', '{%comment%}', ' '], tokenize(' {%comment%} ')
assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], tokenize(' {%comment%} {%endcomment%} ')
assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ")
end
def test_tokenize_incomplete_end
assert_tokens 'before{{ incomplete }after', ['before', '{{ incomplete }', 'after']
assert_tokens 'before{% incomplete %after', ['before', '{%', ' incomplete %after']
end
def test_tokenize_no_end
assert_tokens 'before{{ unterminated ', ['before', '{{', ' unterminated ']
assert_tokens 'before{% unterminated ', ['before', '{%', ' unterminated ']
end
private
def assert_tokens(source, expected)
assert_equal expected, tokenize(source)
assert_equal expected, old_tokenize(source)
end
def tokenize(source)
tokenizer = Liquid::Tokenizer.new(source)
tokens = []
while token = tokenizer.next
tokens << token
end
tokens
end
AnyStartingTag = /\{\{|\{\%/
VariableIncompleteEnd = /\}\}?/
PartialTemplateParser = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}|#{Liquid::VariableStart}.*?#{VariableIncompleteEnd}/o
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/o
def old_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

View File

@@ -73,14 +73,8 @@ class VariableTest < Test::Unit::TestCase
end
def test_symbol
var = Variable.new("http://disney.com/logo.gif | image: 'med' ", :error_mode => :lax)
assert_equal "http://disney.com/logo.gif", var.name
assert_equal [["image",["'med'"]]], var.filters
end
def test_string_to_filter
var = Variable.new("'http://disney.com/logo.gif' | image: 'med' ")
assert_equal "'http://disney.com/logo.gif'", var.name
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
end
@@ -121,18 +115,10 @@ class VariableTest < Test::Unit::TestCase
end
def test_lax_filter_argument_parsing
var = Variable.new(%! number_of_comments | pluralize: 'comment': 'comments' !, :error_mode => :lax)
var = Variable.new(%! number_of_comments | pluralize: 'comment': 'comments' !)
assert_equal 'number_of_comments', var.name
assert_equal [['pluralize',["'comment'","'comments'"]]], var.filters
end
def test_strict_filter_argument_parsing
with_error_mode(:strict) do
assert_raises(SyntaxError) do
Variable.new(%! number_of_comments | pluralize: 'comment': 'comments' !)
end
end
end
end

View File

@@ -2,26 +2,16 @@
require 'test/unit'
require 'test/unit/assertions'
$:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib'))
require 'liquid.rb'
mode = :strict
if env_mode = ENV['LIQUID_PARSER_MODE']
puts "-- #{env_mode.upcase} ERROR MODE"
mode = env_mode.to_sym
begin
require 'ruby-debug'
rescue LoadError
puts "Couldn't load ruby-debug. gem install ruby-debug if you need it."
end
Liquid::Template.error_mode = mode
require File.join(File.dirname(__FILE__), '..', 'lib', 'liquid')
module Test
module Unit
class TestCase
def fixture(name)
File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", name)
end
end
module Assertions
include Liquid
@@ -34,20 +24,6 @@ module Test
assert_match expected, Template.parse(template).render(assigns)
end
def assert_match_syntax_error(match, template, registers = {})
exception = assert_raise(Liquid::SyntaxError) {
Template.parse(template).render(assigns)
}
assert_match match, exception.message
end
def with_error_mode(mode)
old_mode = Liquid::Template.error_mode
Liquid::Template.error_mode = mode
yield
Liquid::Template.error_mode = old_mode
end
end # Assertions
end # Unit
end # Test