Compare commits

..

20 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
bb954bce1e Use StringSlice.join on the rendered results. 2014-02-28 14:12:57 -05:00
Dylan Thacker-Smith
cc0276bb97 Add a StringSlice class to use in the nodelist. 2014-02-28 13:16:07 -05:00
Dylan Thacker-Smith
03d586aafe Add convenience methods for getting a struct from a ruby object.
If we are trying to get the struct from something other than self, then we
should make sure to check the class of the object.  This util functions
make this easier.
2014-02-28 10:08:55 -05:00
Dylan Thacker-Smith
dc8a34a52f Implement Block#parse_body in C. 2014-02-28 07:47:36 -05:00
Dylan Thacker-Smith
99cebf74bc Rename Block#parse to parse_body since that is how it is being used. 2014-02-27 23:16:11 -05:00
Dylan Thacker-Smith
7eb64886dc Move the parse method out of Tag, only blocks need the body parsed.
The parse method should be renamed to something like parse_body,
since that is how it is used, and no non-block tags were using the
parse method.
2014-02-27 22:31:09 -05:00
Dylan Thacker-Smith
f89046e81f Use super rather than render_all in single block render classes. 2014-02-27 21:38:49 -05:00
Dylan Thacker-Smith
9ee4573ef4 Avoid keeping track of two lists of nodes during parsing. 2014-02-27 20:51:05 -05:00
Dylan Thacker-Smith
a48b4f47f6 Return nil in Document#block_delimiter rather than an empty array.
The block delimiter is normally a string, so nil makes more sense when
there is no delimiter. We also don't want to allocate an array for no
reason.
2014-02-27 20:06:57 -05:00
Dylan Thacker-Smith
72d402837e Remove unused Block#end_tag method.
Although the method is called, it is defined with an empty body and not
overridden to do anything else.
2014-02-27 18:53:18 -05:00
Dylan Thacker-Smith
06bef40527 Fix a missing return warning. 2014-02-27 18:47:55 -05:00
Dylan Thacker-Smith
a48b245e6e Turn on C compiler warnings. 2014-02-27 18:45:57 -05:00
Dylan Thacker-Smith
d4aabda625 Avoid freeing of uninitialized memory.
Thanks to Isha for pointing this out.
2014-02-27 18:32:19 -05:00
Dylan Thacker-Smith
dab6bdfdee Make sure the ext directory is included in the distributed gem. 2014-02-27 15:50:43 -05:00
Dylan Thacker-Smith
8c075fca1f Remove a couple FIXME comments which are only partially a lie.
I added those comments before creating an invalid token type to return the
error. However, we still aren't making use of the token type.
2014-02-27 15:21:57 -05:00
Dylan Thacker-Smith
ea8406e36e Create a Liquid::Tokenizer class in the C extension. 2014-02-27 15:20:22 -05:00
Dylan Thacker-Smith
8bb3bca64a Require the liquid extension when liquid is required. 2014-02-27 14:22:18 -05:00
Dylan Thacker-Smith
5de1082201 Add profile:stackprof rake task. 2014-02-27 11:20:49 -05:00
Dylan Thacker-Smith
7ba02d2811 Use start and end of string rather than line matching in regexes. 2014-02-27 10:07:04 -05:00
Dylan Thacker-Smith
2066676bf4 Add a C extension that doesn't yet do anything. 2014-02-27 09:58:33 -05:00
78 changed files with 1154 additions and 736 deletions

4
.gitignore vendored
View File

@@ -5,8 +5,6 @@ pkg
*.rbc *.rbc
.rvmrc .rvmrc
.ruby-version .ruby-version
Gemfile.lock
/ext/liquid/Makefile
*.o
*.bundle *.bundle
/tmp /tmp
Gemfile.lock

View File

@@ -3,10 +3,6 @@
## 3.0.0 / not yet released / branch "master" ## 3.0.0 / not yet released / branch "master"
* ... * ...
* Freeze lots of string literals for new Ruby 2.1 optimization, see #297 [Florian Weingarten, fw42]
* Allow newlines in tags and variables, see #324 [Dylan Thacker-Smith, dylanahsmith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
* Add a to_s default for liquid drops, see #306 [Adam Doeler, releod] * Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42] * 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] * Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
@@ -25,6 +21,7 @@
* Fix clashing method names in enumerable drops, see #238 [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] * Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
* Improved whitespace stripping for blank blocks, related to #216 [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" ## 2.6.0 / 2013-11-25 / branch "2.6-stable"

View File

@@ -1,5 +1,6 @@
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
require 'rake/extensiontask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version" require "liquid/version"
@@ -64,9 +65,13 @@ namespace :profile do
ruby "./performance/profile.rb" ruby "./performance/profile.rb"
end end
desc "Run the liquid profile/performance coverage with strict parsing" task :stackprof do
task :strict do ruby "./performance/stackprof.rb"
ruby "./performance/profile.rb strict" end
desc "Run KCacheGrind"
task :grind => :run do
system "qcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
end end
end end
@@ -76,10 +81,7 @@ task :example do
ruby "-w -d -Ilib example/server/server.rb" ruby "-w -d -Ilib example/server/server.rb"
end end
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' Rake::ExtensionTask.new "liquid" do |ext|
require 'rake/extensiontask' ext.lib_dir = "lib/liquid"
Rake::ExtensionTask.new "liquid" do |ext|
ext.lib_dir = "lib/liquid"
end
Rake::Task[:test].prerequisites << :compile
end end
Rake::Task[:test].prerequisites << :compile

167
ext/liquid/block.c Normal file
View File

@@ -0,0 +1,167 @@
#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);
}

8
ext/liquid/block.h Normal file
View File

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

View File

@@ -1,4 +1,3 @@
require 'mkmf' require 'mkmf'
$CFLAGS << ' -Wall -Werror' $CFLAGS << ' -Wall'
$warnflags.gsub!(/-Wdeclaration-after-statement/, "")
create_makefile("liquid/liquid") create_makefile("liquid/liquid")

View File

@@ -1,9 +0,0 @@
#include "liquid.h"
VALUE mLiquid;
void Init_liquid(void)
{
mLiquid = rb_define_module("Liquid");
init_liquid_tokenizer();
}

View File

@@ -1,11 +0,0 @@
#ifndef LIQUID_H
#define LIQUID_H
#include <ruby.h>
#include <stdbool.h>
#include "tokenizer.h"
extern VALUE mLiquid;
#endif

18
ext/liquid/liquid_ext.c Normal file
View File

@@ -0,0 +1,18 @@
#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();
}

17
ext/liquid/liquid_ext.h Normal file
View File

@@ -0,0 +1,17 @@
#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

167
ext/liquid/slice.c Normal file
View File

@@ -0,0 +1,167 @@
#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);
}

18
ext/liquid/slice.h Normal file
View File

@@ -0,0 +1,18 @@
#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,67 +1,44 @@
#include "liquid.h" #include "liquid_ext.h"
VALUE cLiquidTokenizer; VALUE cLiquidTokenizer;
static void tokenizer_mark(void *ptr) { static void free_tokenizer(void *ptr)
tokenizer_t *tokenizer = ptr;
rb_gc_mark(tokenizer->source);
}
static void tokenizer_free(void *ptr)
{ {
tokenizer_t *tokenizer = ptr; struct liquid_tokenizer *tokenizer = ptr;
xfree(tokenizer); xfree(tokenizer);
} }
static size_t tokenizer_memsize(const void *ptr) static VALUE rb_allocate(VALUE klass)
{
return ptr ? sizeof(tokenizer_t) : 0;
}
const rb_data_type_t tokenizer_data_type = {
"liquid_tokenizer",
{tokenizer_mark, tokenizer_free, tokenizer_memsize,},
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
#endif
};
static VALUE tokenizer_allocate(VALUE klass)
{ {
VALUE obj; VALUE obj;
tokenizer_t *tokenizer; struct liquid_tokenizer *tokenizer;
obj = TypedData_Make_Struct(klass, tokenizer_t, &tokenizer_data_type, tokenizer); obj = Data_Make_Struct(klass, struct liquid_tokenizer, NULL, free_tokenizer, tokenizer);
tokenizer->source = Qnil;
return obj; return obj;
} }
static VALUE tokenizer_initialize_method(VALUE self, VALUE source) static VALUE rb_initialize(VALUE self, VALUE source)
{ {
tokenizer_t *tokenizer; struct liquid_tokenizer *tokenizer;
Check_Type(source, T_STRING); source = rb_string_value(&source);
Tokenizer_Get_Struct(self, tokenizer); Data_Get_Struct(self, struct liquid_tokenizer, tokenizer);
source = rb_str_dup_frozen(source);
tokenizer->source = source;
tokenizer->cursor = RSTRING_PTR(source); tokenizer->cursor = RSTRING_PTR(source);
tokenizer->length = RSTRING_LEN(source); tokenizer->length = RSTRING_LEN(source);
return Qnil; return Qnil;
} }
void tokenizer_next(tokenizer_t *tokenizer, token_t *token) void liquid_tokenizer_next(struct liquid_tokenizer *tokenizer, struct token *token)
{ {
if (tokenizer->length <= 0) { if (tokenizer->length <= 0) {
memset(token, 0, sizeof(*token)); memset(token, 0, sizeof(*token));
return; return;
} }
const char *cursor = tokenizer->cursor;
const char *last = cursor + tokenizer->length - 1;
token->str = cursor;
token->type = TOKEN_STRING; token->type = TOKEN_STRING;
char *cursor = tokenizer->cursor;
char *last = tokenizer->cursor + tokenizer->length - 1;
while (cursor < last) { while (cursor < last) {
if (*cursor++ != '{') if (*cursor++ != '{')
continue; continue;
@@ -74,6 +51,7 @@ void tokenizer_next(tokenizer_t *tokenizer, token_t *token)
cursor -= 2; cursor -= 2;
goto found; goto found;
} }
char *incomplete_end = cursor;
token->type = TOKEN_INVALID; token->type = TOKEN_INVALID;
if (c == '%') { if (c == '%') {
while (cursor < last) { while (cursor < last) {
@@ -87,40 +65,38 @@ void tokenizer_next(tokenizer_t *tokenizer, token_t *token)
token->type = TOKEN_TAG; token->type = TOKEN_TAG;
goto found; goto found;
} }
// unterminated tag cursor = incomplete_end;
cursor = tokenizer->cursor + 2;
goto found; goto found;
} else { } else {
while (cursor < last) { while (cursor < last) {
if (*cursor++ != '}') if (*cursor++ != '}')
continue; continue;
if (*cursor++ != '}') { if (*cursor++ != '}') {
// variable incomplete end, used to end raw tags incomplete_end = cursor - 1;
cursor--; continue;
goto found;
} }
token->type = TOKEN_VARIABLE; token->type = TOKEN_VARIABLE;
goto found; goto found;
} }
// unterminated variable cursor = incomplete_end;
cursor = tokenizer->cursor + 2;
goto found; goto found;
} }
} }
cursor = last + 1; cursor = last + 1;
found: found:
token->str = tokenizer->cursor;
token->length = cursor - tokenizer->cursor; token->length = cursor - tokenizer->cursor;
tokenizer->cursor += token->length; tokenizer->cursor += token->length;
tokenizer->length -= token->length; tokenizer->length -= token->length;
} }
static VALUE tokenizer_next_method(VALUE self) static VALUE rb_next(VALUE self)
{ {
tokenizer_t *tokenizer; struct liquid_tokenizer *tokenizer;
Tokenizer_Get_Struct(self, tokenizer); Data_Get_Struct(self, struct liquid_tokenizer, tokenizer);
token_t token; struct token token;
tokenizer_next(tokenizer, &token); liquid_tokenizer_next(tokenizer, &token);
if (token.type == TOKEN_NONE) if (token.type == TOKEN_NONE)
return Qnil; return Qnil;
@@ -130,8 +106,8 @@ static VALUE tokenizer_next_method(VALUE self)
void init_liquid_tokenizer() void init_liquid_tokenizer()
{ {
cLiquidTokenizer = rb_define_class_under(mLiquid, "Tokenizer", rb_cObject); cLiquidTokenizer = rb_define_class_under(mLiquid, "Tokenizer", rb_cObject);
rb_define_alloc_func(cLiquidTokenizer, tokenizer_allocate); rb_define_alloc_func(cLiquidTokenizer, rb_allocate);
rb_define_method(cLiquidTokenizer, "initialize", tokenizer_initialize_method, 1); rb_define_method(cLiquidTokenizer, "initialize", rb_initialize, 1);
rb_define_method(cLiquidTokenizer, "next", tokenizer_next_method, 0); rb_define_method(cLiquidTokenizer, "next", rb_next, 0);
rb_define_alias(cLiquidTokenizer, "shift", "next"); rb_define_alias(cLiquidTokenizer, "shift", "next");
} }

View File

@@ -1,6 +1,8 @@
#ifndef LIQUID_TOKENIZER_H #ifndef LIQUID_TOKENIZER_H
#define LIQUID_TOKENIZER_H #define LIQUID_TOKENIZER_H
extern VALUE cLiquidTokenizer;
enum token_type { enum token_type {
TOKEN_NONE, TOKEN_NONE,
TOKEN_INVALID, TOKEN_INVALID,
@@ -9,23 +11,20 @@ enum token_type {
TOKEN_VARIABLE TOKEN_VARIABLE
}; };
typedef struct token { struct token {
enum token_type type; enum token_type type;
const char *str; char *str;
long length; int length;
} token_t; };
typedef struct tokenizer { struct liquid_tokenizer {
VALUE source; char *cursor;
const char *cursor; int length;
long length; };
} tokenizer_t;
extern VALUE cLiquidTokenizer;
extern const rb_data_type_t tokenizer_data_type;
#define Tokenizer_Get_Struct(obj, sval) TypedData_Get_Struct(obj, tokenizer_t, &tokenizer_data_type, sval)
void init_liquid_tokenizer(); void init_liquid_tokenizer();
void tokenizer_next(tokenizer_t *tokenizer, token_t *token); 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 #endif

21
ext/liquid/utils.c Normal file
View File

@@ -0,0 +1,21 @@
#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);
}

8
ext/liquid/utils.h Normal file
View File

@@ -0,0 +1,8 @@
#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

@@ -21,9 +21,9 @@
module Liquid module Liquid
FilterSeparator = /\|/ FilterSeparator = /\|/
ArgumentSeparator = ','.freeze ArgumentSeparator = ','
FilterArgumentSeparator = ':'.freeze FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.'.freeze VariableAttributeSeparator = '.'
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -32,10 +32,16 @@ module Liquid
VariableEnd = /\}\}/ VariableEnd = /\}\}/
QuotedString = /"[^"]*"|'[^']*'/ QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s|:,]+/
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/o
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/o
SpacelessFilter = /\A(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
end end
require 'liquid/liquid'
require "liquid/version" require "liquid/version"
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
@@ -52,6 +58,7 @@ require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/file_system' require 'liquid/file_system'
require 'liquid/template' require 'liquid/template'
require 'liquid/htmltags'
require 'liquid/standardfilters' require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
require 'liquid/module_ex' require 'liquid/module_ex'
@@ -60,9 +67,3 @@ require 'liquid/utils'
# Load all the tags of the standard library # Load all the tags of the standard library
# #
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f } Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
require 'liquid/liquid'
else
require 'liquid/tokenizer'
end

View File

@@ -1,93 +1,37 @@
module Liquid module Liquid
class Block < Tag class Block < Tag
IsTag = /\A#{TagStart}/o def initialize(tag_name, markup, tokens)
IsVariable = /\A#{VariableStart}/o super
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om parse_body(tokens)
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om end
def blank? def blank?
@blank || false @blank || false
end end
def parse(tokens)
@blank = true
@nodelist ||= []
@nodelist.clear
# All child tags of the current block.
@children = []
while token = tokens.shift
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]
new_tag = tag.parse($1, $2, tokens, @options)
@blank &&= new_tag.blank?
@nodelist << new_tag
@children << new_tag
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.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
end
when IsVariable
new_var = create_variable(token)
@nodelist << new_var
@children << new_var
@blank = false
when ''.freeze
# pass
else
@nodelist << token
@blank &&= (token =~ /\A\s*\z/)
end
end
# 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
# warnings of this block and all sub-tags # warnings of this block and all sub-tags
def warnings def warnings
all_warnings = [] all_warnings = []
all_warnings.concat(@warnings) if @warnings all_warnings.concat(@warnings) if @warnings
(@children || []).each do |node| (nodelist || []).each do |node|
all_warnings.concat(node.warnings || []) all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end end
all_warnings all_warnings
end end
def end_tag
end
def unknown_tag(tag, params, tokens) def unknown_tag(tag, params, tokens)
case tag case tag
when 'else'.freeze when 'else'
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze, raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else",
:block_name => block_name)) :block_name => block_name))
when 'end'.freeze when 'end'
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze, raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter",
:block_name => block_name, :block_name => block_name,
:block_delimiter => block_delimiter)) :block_delimiter => block_delimiter))
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag)) raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag", :tag => tag))
end end
end end
@@ -99,21 +43,22 @@ module Liquid
@tag_name @tag_name
end end
def create_variable(token)
token.scan(ContentOfVariable) do |content|
return Variable.new(content.first, @options)
end
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
end
def render(context) def render(context)
render_all(@nodelist, context) render_all(@nodelist, context)
end end
protected 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! def assert_missing_delimitation!
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name)) raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed", :block_name => block_name))
end end
def render_all(list, context) def render_all(list, context)
@@ -138,7 +83,7 @@ module Liquid
context.increment_used_resources(:render_length_current, token_output) context.increment_used_resources(:render_length_current, token_output)
if context.resource_limits_reached? if context.resource_limits_reached?
context.resource_limits[:reached] = true context.resource_limits[:reached] = true
raise MemoryError.new("Memory limits exceeded".freeze) raise MemoryError.new("Memory limits exceeded")
end end
unless token.is_a?(Block) && token.blank? unless token.is_a?(Block) && token.blank?
output << token_output output << token_output
@@ -150,7 +95,7 @@ module Liquid
end end
end end
output.join StringSlice.join(output)
end end
end end
end end

View File

@@ -8,14 +8,14 @@ module Liquid
# #
class Condition #:nodoc: class Condition #:nodoc:
@@operators = { @@operators = {
'=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) }, '==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
'!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, '!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, '<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<'.freeze => :<, '<' => :<,
'>'.freeze => :>, '>' => :>,
'>='.freeze => :>=, '>=' => :>=,
'<='.freeze => :<=, '<=' => :<=,
'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false } 'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false }
} }
def self.operators def self.operators
@@ -61,7 +61,7 @@ module Liquid
end end
def inspect def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
end end
private private

View File

@@ -29,7 +29,7 @@ module Liquid
end end
def increment_used_resources(key, obj) def increment_used_resources(key, obj)
@resource_limits[key] += if obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash) @resource_limits[key] += if obj.kind_of?(StringSlice) || obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
obj.length obj.length
else else
1 1
@@ -103,7 +103,7 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope={}) def push(new_scope={})
@scopes.unshift(new_scope) @scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100 raise StackLevelError, "Nesting too deep" if @scopes.length > 100
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -151,11 +151,11 @@ module Liquid
private private
LITERALS = { LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil, nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true'.freeze => true, 'true' => true,
'false'.freeze => false, 'false' => false,
'blank'.freeze => :blank?, 'blank' => :blank?,
'empty'.freeze => :empty? 'empty' => :empty?
} }
# Look up variable, either resolve directly after considering the name. We can directly handle # Look up variable, either resolve directly after considering the name. We can directly handle
@@ -171,9 +171,9 @@ module Liquid
LITERALS[key] LITERALS[key]
else else
case key case key
when /\A'(.*)'\z/m # Single quoted strings when /\A'(.*)'\z/ # Single quoted strings
$1 $1
when /\A"(.*)"\z/m # Double quoted strings when /\A"(.*)"\z/ # Double quoted strings
$1 $1
when /\A(-?\d+)\z/ # Integer and floats when /\A(-?\d+)\z/ # Integer and floats
$1.to_i $1.to_i
@@ -218,7 +218,7 @@ module Liquid
# assert_equal 'tobi', @context['hash["name"]'] # assert_equal 'tobi', @context['hash["name"]']
def variable(markup) def variable(markup)
parts = markup.scan(VariableParser) parts = markup.scan(VariableParser)
square_bracketed = /\A\[(.*)\]\z/m square_bracketed = /\A\[(.*)\]\z/
first_part = parts.shift first_part = parts.shift
@@ -244,7 +244,7 @@ module Liquid
# Some special cases. If the part wasn't in square brackets and # Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls # no key with the same name was found we interpret following calls
# as commands and call them on the current object # as commands and call them on the current object
elsif !part_resolved and object.respond_to?(part) and ['size'.freeze, 'first'.freeze, 'last'.freeze].include?(part) elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
object = object.send(part.intern).to_liquid object = object.send(part.intern).to_liquid

View File

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

View File

@@ -44,7 +44,7 @@ module Liquid
class LocalFileSystem class LocalFileSystem
attr_accessor :root attr_accessor :root
def initialize(root, pattern = "_%s.liquid".freeze) def initialize(root, pattern = "_%s.liquid")
@root = root @root = root
@pattern = pattern @pattern = pattern
end end
@@ -59,7 +59,7 @@ module Liquid
def full_path(template_path) 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[^.\/][a-zA-Z0-9_\/]+\z/
full_path = if template_path.include?('/'.freeze) 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), @pattern % File.basename(template_path))
else else
File.join(root, @pattern % template_path) File.join(root, @pattern % template_path)

74
lib/liquid/htmltags.rb Normal file
View File

@@ -0,0 +1,74 @@
module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
def initialize(tag_name, markup, tokens)
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row"))
end
super
end
def render(context)
collection = context[@collection_name] or return ''
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)
length = collection.length
cols = context[@attributes['cols']].to_i
row = 1
col = 0
result = "<tr class=\"row1\">\n"
context.stack do
collection.each_with_index do |item, index|
context[@variable_name] = item
context['tablerowloop'] = {
'length' => length,
'index' => index + 1,
'index0' => index,
'col' => col + 1,
'col0' => col,
'index0' => index,
'rindex' => length - index,
'rindex0' => length - index - 1,
'first' => (index == 0),
'last' => (index == length - 1),
'col_first' => (col == 0),
'col_last' => (col == cols - 1)
}
col += 1
result << "<td class=\"col#{col}\">" << super << '</td>'
if col == cols and (index != length - 1)
col = 0
row += 1
result << "</tr>\n<tr class=\"row#{row}\">"
end
end
end
result << "</tr>\n"
result
end
end
Template.register_tag('tablerow', TableRow)
end

View File

@@ -6,7 +6,7 @@ module Liquid
class TranslationError < StandardError class TranslationError < StandardError
end end
attr_reader :path attr_reader :path
def initialize(path = DEFAULT_LOCALE) def initialize(path = DEFAULT_LOCALE)
@@ -31,7 +31,7 @@ module Liquid
end end
def deep_fetch_translation(name) def deep_fetch_translation(name)
name.split('.'.freeze).reduce(locale) do |level, cur| name.split('.').reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}" level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
end end
end end

View File

@@ -5,7 +5,7 @@ module Liquid
attr_reader :message attr_reader :message
def initialize(message=nil) def initialize(message=nil)
@message = message || "interrupt".freeze @message = message || "interrupt"
end end
end end

View File

@@ -2,14 +2,14 @@ require "strscan"
module Liquid module Liquid
class Lexer class Lexer
SPECIALS = { SPECIALS = {
'|'.freeze => :pipe, '|' => :pipe,
'.'.freeze => :dot, '.' => :dot,
':'.freeze => :colon, ':' => :colon,
','.freeze => :comma, ',' => :comma,
'['.freeze => :open_square, '[' => :open_square,
']'.freeze => :close_square, ']' => :close_square,
'('.freeze => :open_round, '(' => :open_round,
')'.freeze => :close_round ')' => :close_round
} }
IDENTIFIER = /[\w\-?!]+/ IDENTIFIER = /[\w\-?!]+/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/

View File

@@ -66,11 +66,10 @@ module Liquid
str = "" str = ""
# might be a keyword argument (identifier: expression) # might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1) if look(:id) && look(:colon, 1)
str << consume << consume << ' '.freeze str << consume << consume << ' '
end end
str << expression str << expression
str
end end
def variable_signature def variable_signature

View File

@@ -4,17 +4,12 @@ require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
HTML_ESCAPE = { HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
'&'.freeze => '&amp;'.freeze,
'>'.freeze => '&gt;'.freeze,
'<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze
}
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
# Return the size of an array or of an string # Return the size of an array or of an string
def size(input) def size(input)
input.respond_to?(:size) ? input.size : 0 input.respond_to?(:size) ? input.size : 0
end end
@@ -44,19 +39,19 @@ module Liquid
alias_method :h, :escape alias_method :h, :escape
# Truncate a string down to x characters # Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze) def truncate(input, length = 50, truncate_string = "...")
if input.nil? then return end if input.nil? then return end
l = length.to_i - truncate_string.length l = length.to_i - truncate_string.length
l = 0 if l < 0 l = 0 if l < 0
input.length > length.to_i ? input[0...l] + truncate_string : input input.length > length.to_i ? input[0...l] + truncate_string : input
end end
def truncatewords(input, words = 15, truncate_string = "...".freeze) def truncatewords(input, words = 15, truncate_string = "...")
if input.nil? then return end if input.nil? then return end
wordlist = input.to_s.split wordlist = input.to_s.split
l = words.to_i - 1 l = words.to_i - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
end end
# Split input string into an array of substrings separated by given pattern. # Split input string into an array of substrings separated by given pattern.
@@ -81,17 +76,16 @@ module Liquid
end end
def strip_html(input) def strip_html(input)
empty = ''.freeze input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
end end
# Remove all newlines from the string # Remove all newlines from the string
def strip_newlines(input) def strip_newlines(input)
input.to_s.gsub(/\r?\n/, ''.freeze) input.to_s.gsub(/\r?\n/, '')
end end
# Join elements of the array with certain character between them # Join elements of the array with certain character between them
def join(input, glue = ' '.freeze) def join(input, glue = ' ')
[input].flatten.join(glue) [input].flatten.join(glue)
end end
@@ -101,7 +95,7 @@ module Liquid
ary = flatten_if_necessary(input) ary = flatten_if_necessary(input)
if property.nil? if property.nil?
ary.sort ary.sort
elsif ary.first.respond_to?('[]'.freeze) and !ary.first[property].nil? elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
ary.sort {|a,b| a[property] <=> b[property] } ary.sort {|a,b| a[property] <=> b[property] }
elsif ary.first.respond_to?(property) elsif ary.first.respond_to?(property)
ary.sort {|a,b| a.send(property) <=> b.send(property) } ary.sort {|a,b| a.send(property) <=> b.send(property) }
@@ -119,7 +113,7 @@ module Liquid
flatten_if_necessary(input).map do |e| flatten_if_necessary(input).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == "to_liquid".freeze if property == "to_liquid"
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
e[property] e[property]
@@ -128,23 +122,23 @@ module Liquid
end end
# Replace occurrences of a string with another # Replace occurrences of a string with another
def replace(input, string, replacement = ''.freeze) def replace(input, string, replacement = '')
input.to_s.gsub(string, replacement.to_s) input.to_s.gsub(string, replacement.to_s)
end end
# Replace the first occurrences of a string with another # Replace the first occurrences of a string with another
def replace_first(input, string, replacement = ''.freeze) def replace_first(input, string, replacement = '')
input.to_s.sub(string, replacement.to_s) input.to_s.sub(string, replacement.to_s)
end end
# remove a substring # remove a substring
def remove(input, string) def remove(input, string)
input.to_s.gsub(string, ''.freeze) input.to_s.gsub(string, '')
end end
# remove the first occurrences of a substring # remove the first occurrences of a substring
def remove_first(input, string) def remove_first(input, string)
input.to_s.sub(string, ''.freeze) input.to_s.sub(string, '')
end end
# add one string to another # add one string to another
@@ -159,7 +153,7 @@ module Liquid
# Add <br /> tags in front of all newlines in input string # Add <br /> tags in front of all newlines in input string
def newline_to_br(input) def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n".freeze) input.to_s.gsub(/\n/, "<br />\n")
end end
# Reformat a date # Reformat a date
@@ -202,7 +196,7 @@ module Liquid
date = if input.is_a?(String) date = if input.is_a?(String)
case input.downcase case input.downcase
when 'now'.freeze, 'today'.freeze when 'now', 'today'
Time.now Time.now
else else
Time.parse(input) Time.parse(input)
@@ -262,7 +256,7 @@ module Liquid
apply_operation(input, operand, :%) apply_operation(input, operand, :%)
end end
def default(input, default_value = "".freeze) def default(input, default_value = "")
is_blank = input.respond_to?(:empty?) ? input.empty? : !input is_blank = input.respond_to?(:empty?) ? input.empty? : !input
is_blank ? default_value : input is_blank ? default_value : input
end end

View File

@@ -3,23 +3,19 @@ module Liquid
attr_accessor :options attr_accessor :options
attr_reader :nodelist, :warnings attr_reader :nodelist, :warnings
class << self def self.new_with_options(tag_name, markup, tokens, options)
def parse(tag_name, markup, tokens, options) # Forgive me Matz for I have sinned. I know this code is weird
tag = new(tag_name, markup, options) # but it was necessary to maintain API compatibility.
tag.parse(tokens) new_tag = self.allocate
tag new_tag.options = options
end new_tag.send(:initialize, tag_name, markup, tokens)
new_tag
private :new
end end
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
@tag_name = tag_name @tag_name = tag_name
@markup = markup @markup = markup
@options = options @options ||= {} # needs || because might be set before initialize
end
def parse(tokens)
end end
def name def name
@@ -27,7 +23,7 @@ module Liquid
end end
def render(context) def render(context)
''.freeze ''
end end
def blank? def blank?

View File

@@ -9,23 +9,24 @@ module Liquid
# {{ foo }} # {{ foo }}
# #
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
if markup =~ Syntax if markup =~ Syntax
@to = $1 @to = $1
@from = Variable.new($2) @from = Variable.new($2)
else else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze) raise SyntaxError.new options[:locale].t("errors.syntax.assign")
end end
super
end end
def render(context) def render(context)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.increment_used_resources(:assign_score_current, val) context.increment_used_resources(:assign_score_current, val)
''.freeze ''
end end
def blank? def blank?
@@ -33,5 +34,5 @@ module Liquid
end end
end end
Template.register_tag('assign'.freeze, Assign) Template.register_tag('assign', Assign)
end end

View File

@@ -17,5 +17,5 @@ module Liquid
end end
Template.register_tag('break'.freeze, Break) Template.register_tag('break', Break)
end end

View File

@@ -14,20 +14,21 @@ module Liquid
class Capture < Block class Capture < Block
Syntax = /(\w+)/ Syntax = /(\w+)/
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
if markup =~ Syntax if markup =~ Syntax
@to = $1 @to = $1
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture")) raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
end end
super
end end
def render(context) def render(context)
output = super output = super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.increment_used_resources(:assign_score_current, output) context.increment_used_resources(:assign_score_current, output)
''.freeze ''
end end
def blank? def blank?
@@ -35,5 +36,5 @@ module Liquid
end end
end end
Template.register_tag('capture'.freeze, Capture) Template.register_tag('capture', Capture)
end end

View File

@@ -1,17 +1,18 @@
module Liquid module Liquid
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/o
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = $1 @left = $1
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case"))
end end
super
end end
def nodelist def nodelist
@@ -21,9 +22,9 @@ module Liquid
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@nodelist = [] @nodelist = []
case tag case tag
when 'when'.freeze when 'when'
record_when_condition(markup) record_when_condition(markup)
when 'else'.freeze when 'else'
record_else_condition(markup) record_else_condition(markup)
else else
super super
@@ -53,12 +54,12 @@ module Liquid
while markup while markup
# Create a new nodelist and assign it to the new block # Create a new nodelist and assign it to the new block
if not markup =~ WhenSyntax if not markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when"))
end end
markup = $2 markup = $2
block = Condition.new(@left, '=='.freeze, $1) block = Condition.new(@left, '==', $1)
block.attach(@nodelist) block.attach(@nodelist)
@blocks.push(block) @blocks.push(block)
end end
@@ -66,7 +67,7 @@ module Liquid
def record_else_condition(markup) def record_else_condition(markup)
if not markup.strip.empty? if not markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else"))
end end
block = ElseCondition.new block = ElseCondition.new
@@ -75,5 +76,5 @@ module Liquid
end end
end end
Template.register_tag('case'.freeze, Case) Template.register_tag('case', Case)
end end

View File

@@ -1,7 +1,7 @@
module Liquid module Liquid
class Comment < Block class Comment < Block
def render(context) def render(context)
''.freeze ''
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@@ -12,5 +12,5 @@ module Liquid
end end
end end
Template.register_tag('comment'.freeze, Comment) Template.register_tag('comment', Comment)
end end

View File

@@ -1,4 +1,5 @@
module Liquid module Liquid
# Continue tag to be used to break out of a for loop. # Continue tag to be used to break out of a for loop.
# #
# == Basic Usage: # == Basic Usage:
@@ -9,10 +10,12 @@ module Liquid
# {% endfor %} # {% endfor %}
# #
class Continue < Tag class Continue < Tag
def interrupt def interrupt
ContinueInterrupt.new ContinueInterrupt.new
end end
end end
Template.register_tag('continue'.freeze, Continue) Template.register_tag('continue', Continue)
end end

View File

@@ -13,10 +13,9 @@ module Liquid
# #
class Cycle < Tag class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/o
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string($2) @variables = variables_from_string($2)
@@ -25,8 +24,9 @@ module Liquid
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = "'#{@variables.to_s}'" @name = "'#{@variables.to_s}'"
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.cycle"))
end end
super
end end
def render(context) def render(context)

View File

@@ -19,9 +19,10 @@ module Liquid
# Hello: -3 # Hello: -3
# #
class Decrement < Tag class Decrement < Tag
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
@variable = markup.strip @variable = markup.strip
super
end end
def render(context) def render(context)
@@ -34,5 +35,5 @@ module Liquid
private private
end end
Template.register_tag('decrement'.freeze, Decrement) Template.register_tag('decrement', Decrement)
end end

View File

@@ -46,10 +46,10 @@ module Liquid
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@nodelist = @for_block = [] @nodelist = @for_block = []
super
end end
def nodelist def nodelist
@@ -61,7 +61,7 @@ module Liquid
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'.freeze return super unless tag == 'else'
@nodelist = @else_block = [] @nodelist = @else_block = []
end end
@@ -74,13 +74,13 @@ module Liquid
# Maintains Ruby 1.8.7 String#each behaviour on 1.9 # Maintains Ruby 1.8.7 String#each behaviour on 1.9
return render_else(context) unless iterable?(collection) return render_else(context) unless iterable?(collection)
from = if @attributes['offset'.freeze] == 'continue'.freeze from = if @attributes['offset'] == 'continue'
context.registers[:for][@name].to_i context.registers[:for][@name].to_i
else else
context[@attributes['offset'.freeze]].to_i context[@attributes['offset']].to_i
end end
limit = context[@attributes['limit'.freeze]] limit = context[@attributes['limit']]
to = limit ? limit.to_i + from : nil to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to) segment = Utils.slice_collection(collection, from, to)
@@ -99,16 +99,15 @@ module Liquid
context.stack do context.stack do
segment.each_with_index do |item, index| segment.each_with_index do |item, index|
context[@variable_name] = item context[@variable_name] = item
context['forloop'.freeze] = { context['forloop'] = {
'name'.freeze => @name, 'name' => @name,
'length'.freeze => length, 'length' => length,
'index'.freeze => index + 1, 'index' => index + 1,
'index0'.freeze => index, 'index0' => index,
'rindex'.freeze => length - index, 'rindex' => length - index,
'rindex0'.freeze => length - index - 1, 'rindex0' => length - index - 1,
'first'.freeze => (index == 0), 'first' => (index == 0),
'last'.freeze => (index == length - 1) 'last' => (index == length - 1) }
}
result << render_all(@for_block, context) result << render_all(@for_block, context)
@@ -136,22 +135,22 @@ module Liquid
@attributes[key] = value @attributes[key] = value
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for"))
end end
end end
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in")) unless p.id?('in')
@collection_name = p.expression @collection_name = p.expression
@name = "#{@variable_name}-#{@collection_name}" @name = "#{@variable_name}-#{@collection_name}"
@reversed = p.id?('reversed'.freeze) @reversed = p.id?('reversed')
@attributes = {} @attributes = {}
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) unless attribute = p.id?('limit') || p.id?('offset')
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute"))
end end
p.consume p.consume
val = p.expression val = p.expression
@@ -162,14 +161,14 @@ module Liquid
private private
def render_else(context) def render_else(context)
return @else_block ? [render_all(@else_block, context)] : ''.freeze return @else_block ? [render_all(@else_block, context)] : ''
end end
def iterable?(collection) def iterable?(collection)
collection.respond_to?(:each) || Utils.non_blank_string?(collection) collection.respond_to?(:each) || Utils.non_blank_string?(collection)
end end
end end
Template.register_tag('for'.freeze, For) Template.register_tag('for', For)
end end

View File

@@ -14,10 +14,10 @@ module Liquid
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/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) BOOLEAN_OPERATORS = %w(and or)
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
@blocks = [] @blocks = []
push_block('if'.freeze, markup) push_block('if', markup)
super
end end
def nodelist def nodelist
@@ -25,7 +25,7 @@ module Liquid
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ['elsif'.freeze, 'else'.freeze].include?(tag) if ['elsif', 'else'].include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
@@ -39,14 +39,14 @@ module Liquid
return render_all(block.attachment, context) return render_all(block.attachment, context)
end end
end end
''.freeze ''
end end
end end
private private
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else'.freeze block = if tag == 'else'
ElseCondition.new ElseCondition.new
else else
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@@ -58,17 +58,17 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators).reverse expressions = markup.scan(ExpressionsAndOperators).reverse
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift =~ Syntax
condition = Condition.new($1, $2, $3) condition = Condition.new($1, $2, $3)
while not expressions.empty? while not expressions.empty?
operator = (expressions.shift).to_s.strip operator = (expressions.shift).to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.shift.to_s =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift.to_s =~ Syntax
new_condition = Condition.new($1, $2, $3) new_condition = Condition.new($1, $2, $3)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator) raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
end end
@@ -81,7 +81,7 @@ module Liquid
condition = parse_comparison(p) condition = parse_comparison(p)
while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) while op = (p.id?('and') || p.id?('or'))
new_cond = parse_comparison(p) new_cond = parse_comparison(p)
new_cond.send(op, condition) new_cond.send(op, condition)
condition = new_cond condition = new_cond
@@ -102,5 +102,5 @@ module Liquid
end end
end end
Template.register_tag('if'.freeze, If) Template.register_tag('if', If)
end end

View File

@@ -4,17 +4,17 @@ module Liquid
def render(context) def render(context)
context.stack do context.stack do
output = render_all(@nodelist, context) output = super
if output != context.registers[:ifchanged] if output != context.registers[:ifchanged]
context.registers[:ifchanged] = output context.registers[:ifchanged] = output
output output
else else
''.freeze ''
end end
end end
end end
end end
Template.register_tag('ifchanged'.freeze, Ifchanged) Template.register_tag('ifchanged', Ifchanged)
end end

View File

@@ -17,9 +17,7 @@ module Liquid
class Include < Tag class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
if markup =~ Syntax if markup =~ Syntax
@template_name = $1 @template_name = $1
@@ -31,11 +29,10 @@ module Liquid
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze)) raise SyntaxError.new(options[:locale].t("errors.syntax.include"))
end end
end
def parse(tokens) super
end end
def blank? def blank?
@@ -51,7 +48,7 @@ module Liquid
context[key] = context[value] context[key] = context[value]
end end
context_variable_name = @template_name[1..-2].split('/'.freeze).last context_variable_name = @template_name[1..-2].split('/').last
if variable.is_a?(Array) if variable.is_a?(Array)
variable.collect do |var| variable.collect do |var|
context[context_variable_name] = var context[context_variable_name] = var
@@ -94,5 +91,5 @@ module Liquid
end end
end end
Template.register_tag('include'.freeze, Include) Template.register_tag('include', Include)
end end

View File

@@ -15,9 +15,9 @@ module Liquid
# Hello: 2 # Hello: 2
# #
class Increment < Tag class Increment < Tag
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
@variable = markup.strip @variable = markup.strip
super
end end
def render(context) def render(context)
@@ -31,5 +31,5 @@ module Liquid
end end
end end
Template.register_tag('increment'.freeze, Increment) Template.register_tag('increment', Increment)
end end

View File

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

View File

@@ -1,73 +0,0 @@
module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@variable_name = $1
@collection_name = $2
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = value
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
end
end
def render(context)
collection = context[@collection_name] or return ''.freeze
from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0
to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil
collection = Utils.slice_collection(collection, from, to)
length = collection.length
cols = context[@attributes['cols'.freeze]].to_i
row = 1
col = 0
result = "<tr class=\"row1\">\n"
context.stack do
collection.each_with_index do |item, index|
context[@variable_name] = item
context['tablerowloop'.freeze] = {
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'col'.freeze => col + 1,
'col0'.freeze => col,
'index0'.freeze => index,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'col_first'.freeze => (col == 0),
'col_last'.freeze => (col == cols - 1)
}
col += 1
result << "<td class=\"col#{col}\">" << render_all(@nodelist, context) << '</td>'
if col == cols and (index != length - 1)
col = 0
row += 1
result << "</tr>\n<tr class=\"row#{row}\">"
end
end
end
result << "</tr>\n"
result
end
end
Template.register_tag('tablerow'.freeze, TableRow)
end

View File

@@ -1,6 +1,7 @@
require File.dirname(__FILE__) + '/if' require File.dirname(__FILE__) + '/if'
module Liquid module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic. # Unless is a conditional just like 'if' but works on the inverse logic.
# #
# {% unless x < 0 %} x is greater than zero {% end %} # {% unless x < 0 %} x is greater than zero {% end %}
@@ -22,10 +23,11 @@ module Liquid
end end
end end
''.freeze ''
end end
end end
end end
Template.register_tag('unless'.freeze, Unless)
Template.register_tag('unless', Unless)
end end

View File

@@ -72,7 +72,7 @@ module Liquid
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options)) @root = Document.new(tokenize(source), DEFAULT_OPTIONS.merge(options))
@warnings = nil @warnings = nil
self self
end end
@@ -110,7 +110,7 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application # filters and tags and might be useful to integrate liquid more with its host application
# #
def render(*args) def render(*args)
return ''.freeze if @root.nil? return '' if @root.nil?
context = case args.first context = case args.first
when Liquid::Context when Liquid::Context

View File

@@ -1,20 +0,0 @@
module Liquid
class Tokenizer
VariableIncompleteEnd = /\}\}?/
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
def initialize(source)
@tokens = source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
@tokens.shift if @tokens[0] && @tokens[0].empty?
end
def next
@tokens.shift
end
alias_method :shift, :next
end
end

View File

@@ -10,7 +10,7 @@ module Liquid
end end
def self.non_blank_string?(collection) def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''.freeze collection.is_a?(String) && collection != ''
end end
def self.slice_collection_using_each(collection, from, to) def self.slice_collection_using_each(collection, from, to)

View File

@@ -19,6 +19,7 @@ module Liquid
@markup = markup @markup = markup
@name = nil @name = nil
@options = options || {} @options = options || {}
case @options[:error_mode] || Template.error_mode case @options[:error_mode] || Template.error_mode
when :strict then strict_parse(markup) when :strict then strict_parse(markup)
@@ -36,9 +37,9 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
@filters = [] @filters = []
if match = markup.match(/\s*(#{QuotedFragment})(.*)/om) if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
@name = match[1] @name = match[1]
if match[2].match(/#{FilterSeparator}\s*(.*)/om) if match[2].match(/#{FilterSeparator}\s*(.*)/o)
filters = Regexp.last_match(1).scan(FilterParser) filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f| filters.each do |f|
if matches = f.match(/\s*(\w+)/) if matches = f.match(/\s*(\w+)/)
@@ -62,7 +63,7 @@ module Liquid
@filters = [] @filters = []
p = Parser.new(markup) p = Parser.new(markup)
# Could be just filters with no input # Could be just filters with no input
@name = p.look(:pipe) ? ''.freeze : p.expression @name = p.look(:pipe) ? '' : p.expression
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@@ -85,7 +86,7 @@ module Liquid
end end
def render(context) def render(context)
return ''.freeze if @name.nil? return '' if @name.nil?
@filters.inject(context[@name]) do |output, filter| @filters.inject(context[@name]) do |output, filter|
filterargs = [] filterargs = []
keyword_args = {} keyword_args = {}

View File

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

View File

@@ -19,16 +19,14 @@ Gem::Specification.new do |s|
s.test_files = Dir.glob("{test}/**/*") s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib,ext}/**/*") + %w(MIT-LICENSE README.md) s.files = Dir.glob("{lib,ext}/**/*") + %w(MIT-LICENSE README.md)
s.extensions = ['ext/liquid/extconf.rb']
s.extra_rdoc_files = ["History.md", "README.md"] s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib" s.require_path = "lib"
s.add_development_dependency 'rake-compiler'
s.add_development_dependency 'stackprof'
s.add_development_dependency 'rake' s.add_development_dependency 'rake'
s.add_development_dependency 'activesupport' s.add_development_dependency 'activesupport'
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
s.extensions = ['ext/liquid/extconf.rb']
s.add_development_dependency 'rake-compiler'
s.add_development_dependency 'stackprof' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.1.0")
end
end end

View File

@@ -1,13 +1,19 @@
require 'stackprof' rescue fail("install stackprof extension/gem") require 'rubygems'
require 'ruby-prof' rescue fail("install ruby-prof extension/gem")
require File.dirname(__FILE__) + '/theme_runner' require File.dirname(__FILE__) + '/theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.run
results = StackProf.run(mode: :cpu) do puts 'Running profiler...'
100.times do
profiler.run results = profiler.run_profile
end
puts 'Success'
puts
[RubyProf::FlatPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter, RubyProf::DotPrinter].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) }
$stderr.puts "wrote #{klass.name} output to #{filename}"
end end
StackProf::Report.new(results).print_text(false, 20)
File.write(ENV['FILENAME'], Marshal.dump(results)) if ENV['FILENAME']

View File

@@ -1,15 +1,15 @@
class CommentForm < Liquid::Block class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/ Syntax = /(#{Liquid::VariableSignature}+)/
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = $1
@attributes = {} @attributes = {}
else else
raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]") raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
end end
super
end end
def render(context) def render(context)

View File

@@ -1,9 +1,7 @@
class Paginate < Liquid::Block class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, options) def initialize(tag_name, markup, tokens)
super
@nodelist = [] @nodelist = []
if markup =~ Syntax if markup =~ Syntax
@@ -21,6 +19,8 @@ class Paginate < Liquid::Block
else else
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number") raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
end end
super
end end
def render(context) def render(context)

View File

@@ -45,11 +45,11 @@ module ShopFilter
end end
def url_for_vendor(vendor_title) def url_for_vendor(vendor_title)
"/collections/#{to_handle(vendor_title)}" "/collections/#{vendor_title.to_handle}"
end end
def url_for_type(type_title) def url_for_type(type_title)
"/collections/#{to_handle(type_title)}" "/collections/#{type_title.to_handle}"
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
@@ -95,16 +95,4 @@ module ShopFilter
input == 1 ? singular : plural input == 1 ? singular : plural
end end
private
def to_handle(str)
result = str.dup
result.downcase!
result.delete!("'\"()[]")
result.gsub!(/\W+/, '-')
result.gsub!(/-+\z/, '') if result[-1] == '-'
result.gsub!(/\A-+/, '') if result[0] == '-'
result
end
end end

15
performance/stackprof.rb Normal file
View File

@@ -0,0 +1,15 @@
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

@@ -8,7 +8,6 @@
require 'rubygems' require 'rubygems'
require 'active_support' require 'active_support'
require 'active_support/json'
require 'yaml' require 'yaml'
require 'digest/md5' require 'digest/md5'
require File.dirname(__FILE__) + '/shopify/liquid' require File.dirname(__FILE__) + '/shopify/liquid'
@@ -65,17 +64,52 @@ class ThemeRunner
end end
def run_profile
RubyProf.measure_mode = RubyProf::WALL_TIME
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
html = nil
page_template = File.basename(template_name, File.extname(template_name))
unless @started
RubyProf.start
RubyProf.pause
@started = true
end
html = nil
RubyProf.resume
html = compile_and_render(liquid, layout, assigns, page_template, template_name)
RubyProf.pause
# return the result and the MD5 of the content, this can be used to detect regressions between liquid version
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)]
# Uncomment to dump html files to /tmp so that you can inspect for errors
# File.open("/tmp/#{File.basename(template_name)}.html", "w+") { |fp| fp <<html}
end
RubyProf.stop
end
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template, template_file)
tmpl = Liquid::Template.new tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
content_for_layout = tmpl.parse(template).render!(assigns) content_for_layout = tmpl.parse(template).render(assigns)
if layout if layout
assigns['content_for_layout'] = content_for_layout assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render!(assigns) tmpl.parse(layout).render(assigns)
else else
content_for_layout content_for_layout
end end

View File

@@ -93,9 +93,9 @@ class BlankTest < Test::Unit::TestCase
def test_include_is_blank def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new Liquid::Template.file_system = BlankTestFileSystem.new
assert_template_result "foobar"*(N+1), wrap("{% include 'foobar' %}") assert_equal "foobar"*(N+1), Template.parse(wrap("{% include 'foobar' %}")).render()
assert_template_result " foobar "*(N+1), wrap("{% include ' foobar ' %}") assert_equal " foobar "*(N+1), Template.parse(wrap("{% include ' foobar ' %}")).render()
assert_template_result " "*(N+1), wrap(" {% include ' ' %} ") assert_equal " ", Template.parse(" {% include ' ' %} ").render()
end end
def test_case_is_blank def test_case_is_blank

View File

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

View File

@@ -19,7 +19,7 @@ class CaptureTest < Test::Unit::TestCase
{{var}} {{var}}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render
assert_equal "test-string", rendered.gsub(/\s/, '') assert_equal "test-string", rendered.gsub(/\s/, '')
end end
@@ -34,7 +34,7 @@ class CaptureTest < Test::Unit::TestCase
{{ first }}-{{ second }} {{ first }}-{{ second }}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render
assert_equal "3-3", rendered.gsub(/\s/, '') assert_equal "3-3", rendered.gsub(/\s/, '')
end end
end # CaptureTest end # CaptureTest

View File

@@ -176,8 +176,8 @@ class ContextTest < Test::Unit::TestCase
end end
Template.register_filter(global) Template.register_filter(global)
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render! assert_equal 'Global test', Template.parse("{{'test' | notice }}").render
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local]) assert_equal 'Local test', Template.parse("{{'test' | notice }}").render({}, :filters => [local])
end end
def test_only_intended_filters_make_it_there def test_only_intended_filters_make_it_there

View File

@@ -106,140 +106,140 @@ class DropsTest < Test::Unit::TestCase
def test_product_drop def test_product_drop
assert_nothing_raised do assert_nothing_raised do
tpl = Liquid::Template.parse( ' ' ) tpl = Liquid::Template.parse( ' ' )
tpl.render!('product' => ProductDrop.new) tpl.render('product' => ProductDrop.new)
end end
end end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render('product' => ProductDrop.new)
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new) assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render('product' => ProductDrop.new)
end end
def test_drops_respond_to_to_liquid def test_drops_respond_to_to_liquid
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new) assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render('product' => ProductDrop.new)
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new) assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render('product' => ProductDrop.new)
end end
def test_text_drop def test_text_drop
output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render('product' => ProductDrop.new)
assert_equal ' text1 ', output assert_equal ' text1 ', output
end end
def test_unknown_method def test_unknown_method
output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render('product' => ProductDrop.new)
assert_equal ' method: unknown ', output assert_equal ' method: unknown ', output
end end
def test_integer_argument_drop def test_integer_argument_drop
output = Liquid::Template.parse( ' {{ product.catchall[8] }} ' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.catchall[8] }} ' ).render('product' => ProductDrop.new)
assert_equal ' method: 8 ', output assert_equal ' method: 8 ', output
end end
def test_text_array_drop def test_text_array_drop
output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render('product' => ProductDrop.new)
assert_equal ' text1 text2 ', output assert_equal ' text1 text2 ', output
end end
def test_context_drop def test_context_drop
output = Liquid::Template.parse( ' {{ context.bar }} ' ).render!('context' => ContextDrop.new, 'bar' => "carrot") output = Liquid::Template.parse( ' {{ context.bar }} ' ).render('context' => ContextDrop.new, 'bar' => "carrot")
assert_equal ' carrot ', output assert_equal ' carrot ', output
end end
def test_nested_context_drop def test_nested_context_drop
output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render!('product' => ProductDrop.new, 'foo' => "monkey") output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render('product' => ProductDrop.new, 'foo' => "monkey")
assert_equal ' monkey ', output assert_equal ' monkey ', output
end end
def test_protected def test_protected
output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render!('product' => ProductDrop.new) output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render('product' => ProductDrop.new)
assert_equal ' ', output assert_equal ' ', output
end end
def test_object_methods_not_allowed def test_object_methods_not_allowed
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new) output = Liquid::Template.parse(" {{ product.#{method} }} ").render('product' => ProductDrop.new)
assert_equal ' ', output assert_equal ' ', output
end end
end end
def test_scope def test_scope
assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render!('context' => ContextDrop.new) assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new)
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }) assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] })
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render!('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1])
end end
def test_scope_with_assigns def test_scope_with_assigns
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render!('context' => ContextDrop.new) assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render('context' => ContextDrop.new)
assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render!('context' => ContextDrop.new) assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render('context' => ContextDrop.new)
assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render!('context' => ContextDrop.new) assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render('context' => ContextDrop.new)
end end
def test_scope_from_tags def test_scope_from_tags
assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1])
assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_access_context_from_drop def test_access_context_from_drop
assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render!('context' => ContextDrop.new, 'dummy' => [1,2,3]) assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1,2,3])
end end
def test_enumerable_drop def test_enumerable_drop
assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new) assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_size def test_enumerable_drop_size
assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render!('collection' => EnumerableDrop.new) assert_equal '3', Liquid::Template.parse( '{{collection.size}}').render('collection' => EnumerableDrop.new)
end end
def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names def test_enumerable_drop_will_invoke_before_method_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method| ["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' => 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)
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
end end
def test_some_enumerable_methods_still_get_invoked def test_some_enumerable_methods_still_get_invoked
[ :count, :max ].each do |method| [ :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' => 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)
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 end
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new) assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render('collection' => RealEnumerableDrop.new)
[ :min, :first ].each do |method| [ :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' => 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)
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
end end
def test_empty_string_value_access def test_empty_string_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '') assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => '')
end end
def test_nil_value_access def test_nil_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil) assert_equal '', Liquid::Template.parse('{{ product[value] }}').render('product' => ProductDrop.new, 'value' => nil)
end end
def test_default_to_s_on_drops def test_default_to_s_on_drops
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new) assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render('product' => ProductDrop.new)
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new) assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render('collection' => EnumerableDrop.new)
end end
end # DropsTest end # DropsTest

View File

@@ -113,13 +113,13 @@ class FiltersInTemplate < Test::Unit::TestCase
def test_local_global def test_local_global
Template.register_filter(MoneyFilter) Template.register_filter(MoneyFilter)
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil) assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render(nil, nil)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => CanadianMoneyFilter) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, :filters => CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, :filters => [CanadianMoneyFilter]) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, :filters => [CanadianMoneyFilter])
end end
def test_local_filter_with_deprecated_syntax def test_local_filter_with_deprecated_syntax
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]) assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, [CanadianMoneyFilter])
end end
end # FiltersTest end # FiltersTest

View File

@@ -77,11 +77,11 @@ class ModuleExTest < Test::Unit::TestCase
end end
def test_should_use_regular_objects_as_drops def test_should_use_regular_objects_as_drops
assert_template_result 'allowedA', "{{ a.allowedA }}", 'a'=>@a assert_equal 'allowedA', Liquid::Template.parse("{{ a.allowedA }}").render('a'=>@a)
assert_template_result 'allowedB', "{{ a.chainedB.allowedB }}", 'a'=>@a assert_equal 'allowedB', Liquid::Template.parse("{{ a.chainedB.allowedB }}").render('a'=>@a)
assert_template_result 'allowedC', "{{ a.chainedB.chainedC.allowedC }}", 'a'=>@a assert_equal 'allowedC', Liquid::Template.parse("{{ a.chainedB.chainedC.allowedC }}").render('a'=>@a)
assert_template_result 'another_allowedC', "{{ a.chainedB.chainedC.another_allowedC }}", 'a'=>@a assert_equal 'another_allowedC', Liquid::Template.parse("{{ a.chainedB.chainedC.another_allowedC }}").render('a'=>@a)
assert_template_result '', "{{ a.restricted }}", 'a'=>@a assert_equal '', Liquid::Template.parse("{{ a.restricted }}").render('a'=>@a)
assert_template_result '', "{{ a.unknown }}", 'a'=>@a assert_equal '', Liquid::Template.parse("{{ a.unknown }}").render('a'=>@a)
end end
end # ModuleExTest end # ModuleExTest

View File

@@ -41,76 +41,76 @@ class OutputTest < Test::Unit::TestCase
text = %| {{best_cars}} | text = %| {{best_cars}} |
expected = %| bmw | expected = %| bmw |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render(@assigns)
end end
def test_variable_traversing def test_variable_traversing
text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} | text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} |
expected = %| good bad good | expected = %| good bad good |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render(@assigns)
end end
def test_variable_piping def test_variable_piping
text = %( {{ car.gm | make_funny }} ) text = %( {{ car.gm | make_funny }} )
expected = %| LOL | expected = %| LOL |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_input def test_variable_piping_with_input
text = %( {{ car.gm | cite_funny }} ) text = %( {{ car.gm | cite_funny }} )
expected = %| LOL: bad | expected = %| LOL: bad |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_args def test_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' }} ! text = %! {{ car.gm | add_smiley : ':-(' }} !
expected = %| bad :-( | expected = %| bad :-( |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_no_args def test_variable_piping_with_no_args
text = %! {{ car.gm | add_smiley }} ! text = %! {{ car.gm | add_smiley }} !
expected = %| bad :-) | expected = %| bad :-) |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
def test_multiple_variable_piping_with_args def test_multiple_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
expected = %| bad :-( :-( | expected = %| bad :-( :-( |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_multiple_args def test_variable_piping_with_multiple_args
text = %! {{ car.gm | add_tag : 'span', 'bar'}} ! text = %! {{ car.gm | add_tag : 'span', 'bar'}} !
expected = %| <span id="bar">bad</span> | expected = %| <span id="bar">bad</span> |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
def test_variable_piping_with_variable_args def test_variable_piping_with_variable_args
text = %! {{ car.gm | add_tag : 'span', car.bmw}} ! text = %! {{ car.gm | add_tag : 'span', car.bmw}} !
expected = %| <span id="good">bad</span> | expected = %| <span id="good">bad</span> |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
def test_multiple_pipings def test_multiple_pipings
text = %( {{ best_cars | cite_funny | paragraph }} ) text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %| <p>LOL: bmw</p> | expected = %| <p>LOL: bmw</p> |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
def test_link_to def test_link_to
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
expected = %| <a href="http://typo.leetsoft.com">Typo</a> | expected = %| <a href="http://typo.leetsoft.com">Typo</a> |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => [FunnyFilter]) assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter])
end end
end # OutputTest end # OutputTest

View File

@@ -7,8 +7,8 @@ class ParsingQuirksTest < Test::Unit::TestCase
text = %| div { font-weight: bold; } | text = %| div { font-weight: bold; } |
template = Template.parse(text) template = Template.parse(text)
assert_equal text, template.render! assert_equal text, template.render
assert_equal [String], template.root.nodelist.collect {|i| i.class} assert_equal [StringSlice], template.root.nodelist.collect {|i| i.class}
end end
def test_raise_on_single_close_bracet def test_raise_on_single_close_bracet

View File

@@ -13,14 +13,14 @@ class SecurityTest < Test::Unit::TestCase
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %| 1+1 | expected = %| 1+1 |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render(@assigns)
end end
def test_no_existing_instance_eval def test_no_existing_instance_eval
text = %( {{ '1+1' | __instance_eval__ }} ) text = %( {{ '1+1' | __instance_eval__ }} )
expected = %| 1+1 | expected = %| 1+1 |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render(@assigns)
end end
@@ -28,7 +28,7 @@ class SecurityTest < Test::Unit::TestCase
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %| 1+1 | expected = %| 1+1 |
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal expected, Template.parse(text).render(@assigns)
end end
@@ -36,7 +36,7 @@ class SecurityTest < Test::Unit::TestCase
text = %( {{ '1+1' | add_one | instance_eval }} ) text = %( {{ '1+1' | add_one | instance_eval }} )
expected = %| 1+1 + 1 | expected = %| 1+1 + 1 |
assert_equal expected, Template.parse(text).render!(@assigns, :filters => SecurityFilter) assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter)
end end
def test_does_not_add_filters_to_symbol_table def test_does_not_add_filters_to_symbol_table
@@ -47,7 +47,7 @@ class SecurityTest < Test::Unit::TestCase
template = Template.parse(test) template = Template.parse(test)
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
template.render! template.render
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal [], (Symbol.all_symbols - current_symbols)
end end

View File

@@ -126,38 +126,37 @@ class StandardFiltersTest < Test::Unit::TestCase
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
assert_template_result "", '{{ "foo" | map: "__id__" }}' assert_equal "", Liquid::Template.parse('{{ "foo" | map: "__id__" }}').render
assert_template_result "", '{{ "foo" | map: "inspect" }}' assert_equal "", Liquid::Template.parse('{{ "foo" | map: "inspect" }}').render
end end
def test_map_calls_to_liquid def test_map_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_template_result "woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t] assert_equal "woot: 1", Liquid::Template.parse('{{ foo | map: "whatever" }}').render("foo" => [t])
end end
def test_map_on_hashes def test_map_on_hashes
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}', assert_equal "4217", Liquid::Template.parse('{{ thing | map: "foo" | map: "bar" }}').render("thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] })
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] }
end end
def test_sort_calls_to_liquid def test_sort_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_template_result "woot: 1", '{{ foo | sort: "whatever" }}', "foo" => [t] assert_equal "woot: 1", Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
end end
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = Proc.new{ drop } p = Proc.new{ drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_template_result "testfoo", templ, "procs" => [p] assert_equal "testfoo", Liquid::Template.parse(templ).render("procs" => [p])
end end
def test_map_works_on_enumerables def test_map_works_on_enumerables
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new assert_equal "123", Liquid::Template.parse('{{ foo | map: "foo" }}').render!("foo" => TestEnumerable.new)
end end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new assert_equal "213", Liquid::Template.parse('{{ foo | sort: "bar" | map: "foo" }}').render!("foo" => TestEnumerable.new)
end end
def test_date def test_date
@@ -258,7 +257,7 @@ class StandardFiltersTest < Test::Unit::TestCase
assert_template_result "4", "{{ 14 | divided_by:3 }}" assert_template_result "4", "{{ 14 | divided_by:3 }}"
assert_template_result "5", "{{ 15 | divided_by:3 }}" assert_template_result "5", "{{ 15 | divided_by:3 }}"
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render assert_template_result "Liquid error: divided by 0", "{{ 5 | divided_by:0 }}"
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}" assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}"
end end

View File

@@ -0,0 +1,34 @@
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,6 +1,6 @@
require 'test_helper' require 'test_helper'
class TableRowTest < Test::Unit::TestCase class HtmlTagTest < Test::Unit::TestCase
include Liquid include Liquid
class ArrayDrop < Liquid::Drop class ArrayDrop < Liquid::Drop
@@ -15,7 +15,7 @@ class TableRowTest < Test::Unit::TestCase
end end
end end
def test_table_row def test_html_table
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 4 </td><td class=\"col2\"> 5 </td><td class=\"col3\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}',
@@ -26,14 +26,14 @@ class TableRowTest < Test::Unit::TestCase
'numbers' => []) 'numbers' => [])
end end
def test_table_row_with_different_cols def test_html_table_with_different_cols
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td><td class=\"col3\"> 3 </td><td class=\"col4\"> 4 </td><td class=\"col5\"> 5 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 6 </td></tr>\n",
'{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}',
'numbers' => [1,2,3,4,5,6]) 'numbers' => [1,2,3,4,5,6])
end end
def test_table_col_counter def test_html_col_counter
assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n", assert_template_result("<tr class=\"row1\">\n<td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row2\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n<tr class=\"row3\"><td class=\"col1\">1</td><td class=\"col2\">2</td></tr>\n",
'{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}',
'numbers' => [1,2,3,4,5,6]) 'numbers' => [1,2,3,4,5,6])
@@ -60,4 +60,4 @@ class TableRowTest < Test::Unit::TestCase
'{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}',
'numbers' => [0,1,2,3,4,5,6,7]) 'numbers' => [0,1,2,3,4,5,6,7])
end end
end end # HtmlTagTest

View File

@@ -78,52 +78,57 @@ class IncludeTagTest < Test::Unit::TestCase
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal 'from OtherFileSystem', assert_equal 'from OtherFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => OtherFileSystem.new}) Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => OtherFileSystem.new})
end end
def test_include_tag_with def test_include_tag_with
assert_template_result "Product: Draft 151cm ", assert_equal "Product: Draft 151cm ",
"{% include 'product' with products[0] %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] Template.parse("{% include 'product' with products[0] %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] )
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_template_result "Product: Draft 151cm ", assert_equal "Product: Draft 151cm ",
"{% include 'product' %}", "product" => {'title' => 'Draft 151cm'} Template.parse("{% include 'product' %}").render( "product" => {'title' => 'Draft 151cm'} )
end end
def test_include_tag_for def test_include_tag_for
assert_template_result "Product: Draft 151cm Product: Element 155cm ",
"{% include 'product' for products %}", "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] assert_equal "Product: Draft 151cm Product: Element 155cm ",
Template.parse("{% include 'product' for products %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] )
end end
def test_include_tag_with_local_variables def test_include_tag_with_local_variables
assert_template_result "Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}" assert_equal "Locale: test123 ",
Template.parse("{% include 'locale_variables' echo1: 'test123' %}").render
end end
def test_include_tag_with_multiple_local_variables def test_include_tag_with_multiple_local_variables
assert_template_result "Locale: test123 test321", assert_equal "Locale: test123 test321",
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}" Template.parse("{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}").render
end end
def test_include_tag_with_multiple_local_variables_from_context def test_include_tag_with_multiple_local_variables_from_context
assert_template_result "Locale: test123 test321", assert_equal "Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", Template.parse("{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}").render('echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'})
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}
end end
def test_nested_include_tag def test_nested_include_tag
assert_template_result "body body_detail", "{% include 'body' %}" assert_equal "body body_detail",
Template.parse("{% include 'body' %}").render
assert_template_result "header body body_detail footer", "{% include 'nested_template' %}" assert_equal "header body body_detail footer",
Template.parse("{% include 'nested_template' %}").render
end end
def test_nested_include_with_variable def test_nested_include_with_variable
assert_template_result "Product: Draft 151cm details ",
"{% include 'nested_product_template' with product %}", "product" => {"title" => 'Draft 151cm'}
assert_template_result "Product: Draft 151cm details Product: Element 155cm details ", assert_equal "Product: Draft 151cm details ",
"{% include 'nested_product_template' for products %}", "products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}] Template.parse("{% include 'nested_product_template' with product %}").render("product" => {"title" => 'Draft 151cm'})
assert_equal "Product: Draft 151cm details Product: Element 155cm details ",
Template.parse("{% include 'nested_product_template' for products %}").render("products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}])
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
@@ -155,33 +160,34 @@ class IncludeTagTest < Test::Unit::TestCase
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
assert_template_result "Test123", "{% include template %}", "template" => 'Test123'
assert_template_result "Test321", "{% include template %}", "template" => 'Test321'
assert_template_result "Product: Draft 151cm ", "{% include template for product %}", assert_equal "Test123", Template.parse("{% include template %}").render("template" => 'Test123')
"template" => 'product', 'product' => { 'title' => 'Draft 151cm'} assert_equal "Test321", Template.parse("{% include template %}").render("template" => 'Test321')
assert_equal "Product: Draft 151cm ", Template.parse("{% include template for product %}").render("template" => 'product', 'product' => { 'title' => 'Draft 151cm'})
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystemfrom CountingFileSystem', assert_equal 'from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system}) Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render({}, :registers => {:file_system => file_system})
assert_equal 1, file_system.count assert_equal 1, file_system.count
end end
def test_include_tag_doesnt_cache_partials_across_renders def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystem', assert_equal 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system}) Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => file_system})
assert_equal 1, file_system.count assert_equal 1, file_system.count
assert_equal 'from CountingFileSystem', assert_equal 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, :registers => {:file_system => file_system}) Template.parse("{% include 'pick_a_source' %}").render({}, :registers => {:file_system => file_system})
assert_equal 2, file_system.count assert_equal 2, file_system.count
end end
def test_include_tag_within_if_statement def test_include_tag_within_if_statement
assert_template_result "foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}" assert_equal "foo_if_true",
Template.parse("{% if true %}{% include 'foo_if_true' %}{% endif %}").render
end end
def test_custom_include_tag def test_custom_include_tag
@@ -189,7 +195,7 @@ class IncludeTagTest < Test::Unit::TestCase
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal "custom_foo", assert_equal "custom_foo",
Template.parse("{% include 'custom_foo' %}").render! Template.parse("{% include 'custom_foo' %}").render
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -200,7 +206,7 @@ class IncludeTagTest < Test::Unit::TestCase
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal "custom_foo_if_true", assert_equal "custom_foo_if_true",
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render! Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end

View File

@@ -20,6 +20,5 @@ class RawTagTest < Test::Unit::TestCase
assert_template_result ' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}' assert_template_result ' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}'
assert_template_result ' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}' assert_template_result ' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}'
assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}' assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}'
assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}'
end end
end end

View File

@@ -4,7 +4,7 @@ class StandardTagTest < Test::Unit::TestCase
include Liquid include Liquid
def test_tag def test_tag
tag = Tag.parse('tag', [], [], {}) tag = Tag.new('tag', [], [])
assert_equal 'liquid::tag', tag.name assert_equal 'liquid::tag', tag.name
assert_equal '', tag.render(Context.new) assert_equal '', tag.render(Context.new)
end end
@@ -180,11 +180,11 @@ class StandardTagTest < Test::Unit::TestCase
# Example from the shopify forums # Example from the shopify forums
code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }})
template = Liquid::Template.parse(code) template = Liquid::Template.parse(code)
assert_equal "menswear", template.render!("collection" => {'handle' => 'menswear-jackets'}) assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-jackets'})
assert_equal "menswear", template.render!("collection" => {'handle' => 'menswear-t-shirts'}) assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-t-shirts'})
assert_equal "womenswear", template.render!("collection" => {'handle' => 'x'}) assert_equal "womenswear", template.render("collection" => {'handle' => 'x'})
assert_equal "womenswear", template.render!("collection" => {'handle' => 'y'}) assert_equal "womenswear", template.render("collection" => {'handle' => 'y'})
assert_equal "womenswear", template.render!("collection" => {'handle' => 'z'}) assert_equal "womenswear", template.render("collection" => {'handle' => 'z'})
end end
def test_case_when_or def test_case_when_or
@@ -218,7 +218,7 @@ class StandardTagTest < Test::Unit::TestCase
end end
def test_assign def test_assign
assert_template_result 'variable', '{% assign a = "variable"%}{{a}}' assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}').render
end end
def test_assign_unassigned def test_assign_unassigned
@@ -227,11 +227,12 @@ class StandardTagTest < Test::Unit::TestCase
end end
def test_assign_an_empty_string def test_assign_an_empty_string
assert_template_result '', '{% assign a = ""%}{{a}}' assert_equal '', Liquid::Template.parse( '{% assign a = ""%}{{a}}' ).render
end end
def test_assign_is_global def test_assign_is_global
assert_template_result 'variable', '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' assert_equal 'variable',
Liquid::Template.parse( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).render
end end
def test_case_detects_bad_syntax def test_case_detects_bad_syntax
@@ -296,8 +297,4 @@ class StandardTagTest < Test::Unit::TestCase
assigns = {'array' => [ 1, 1, 1, 1] } assigns = {'array' => [ 1, 1, 1, 1] }
assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns)
end end
def test_multiline_tag
assert_template_result '0 1 2 3', "0{%\nfor i in (1..3)\n%} {{\ni\n}}{%\nendfor\n%}"
end
end # StandardTagTest end # StandardTagTest

View File

@@ -4,78 +4,93 @@ class StatementsTest < Test::Unit::TestCase
include Liquid include Liquid
def test_true_eql_true def test_true_eql_true
text = ' {% if true == true %} true {% else %} false {% endif %} ' text = %| {% if true == true %} true {% else %} false {% endif %} |
assert_template_result ' true ', text expected = %| true |
assert_equal expected, Template.parse(text).render
end end
def test_true_not_eql_true def test_true_not_eql_true
text = ' {% if true != true %} true {% else %} false {% endif %} ' text = %| {% if true != true %} true {% else %} false {% endif %} |
assert_template_result ' false ', text expected = %| false |
assert_equal expected, Template.parse(text).render
end end
def test_true_lq_true def test_true_lq_true
text = ' {% if 0 > 0 %} true {% else %} false {% endif %} ' text = %| {% if 0 > 0 %} true {% else %} false {% endif %} |
assert_template_result ' false ', text expected = %| false |
assert_equal expected, Template.parse(text).render
end end
def test_one_lq_zero def test_one_lq_zero
text = ' {% if 1 > 0 %} true {% else %} false {% endif %} ' text = %| {% if 1 > 0 %} true {% else %} false {% endif %} |
assert_template_result ' true ', text expected = %| true |
assert_equal expected, Template.parse(text).render
end end
def test_zero_lq_one def test_zero_lq_one
text = ' {% if 0 < 1 %} true {% else %} false {% endif %} ' text = %| {% if 0 < 1 %} true {% else %} false {% endif %} |
assert_template_result ' true ', text expected = %| true |
assert_equal expected, Template.parse(text).render
end end
def test_zero_lq_or_equal_one def test_zero_lq_or_equal_one
text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} ' text = %| {% if 0 <= 0 %} true {% else %} false {% endif %} |
assert_template_result ' true ', text expected = %| true |
assert_equal expected, Template.parse(text).render
end end
def test_zero_lq_or_equal_one_involving_nil def test_zero_lq_or_equal_one_involving_nil
text = ' {% if null <= 0 %} true {% else %} false {% endif %} ' text = %| {% if null <= 0 %} true {% else %} false {% endif %} |
assert_template_result ' false ', text expected = %| false |
assert_equal expected, Template.parse(text).render
text = ' {% if 0 <= null %} true {% else %} false {% endif %} ' text = %| {% if 0 <= null %} true {% else %} false {% endif %} |
assert_template_result ' false ', text expected = %| false |
assert_equal expected, Template.parse(text).render
end end
def test_zero_lqq_or_equal_one def test_zero_lqq_or_equal_one
text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} ' text = %| {% if 0 >= 0 %} true {% else %} false {% endif %} |
assert_template_result ' true ', text expected = %| true |
assert_equal expected, Template.parse(text).render
end end
def test_strings def test_strings
text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} " text = %| {% if 'test' == 'test' %} true {% else %} false {% endif %} |
assert_template_result ' true ', text expected = %| true |
assert_equal expected, Template.parse(text).render
end end
def test_strings_not_equal def test_strings_not_equal
text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} " text = %| {% if 'test' != 'test' %} true {% else %} false {% endif %} |
assert_template_result ' false ', text expected = %| false |
assert_equal expected, Template.parse(text).render
end end
def test_var_strings_equal def test_var_strings_equal
text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} ' text = %| {% if var == "hello there!" %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'var' => 'hello there!' expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end end
def test_var_strings_are_not_equal def test_var_strings_are_not_equal
text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} ' text = %| {% if "hello there!" == var %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'var' => 'hello there!' expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end end
def test_var_and_long_string_are_equal def test_var_and_long_string_are_equal
text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} " text = %| {% if var == 'hello there!' %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'var' => 'hello there!' expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end end
def test_var_and_long_string_are_equal_backwards def test_var_and_long_string_are_equal_backwards
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " text = %| {% if 'hello there!' == var %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'var' => 'hello there!' expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 'hello there!')
end end
#def test_is_nil #def test_is_nil
@@ -86,28 +101,34 @@ class StatementsTest < Test::Unit::TestCase
#end #end
def test_is_collection_empty def test_is_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = %| {% if array == empty %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'array' => [] expected = %| true |
assert_equal expected, Template.parse(text).render('array' => [])
end end
def test_is_not_collection_empty def test_is_not_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = %| {% if array == empty %} true {% else %} false {% endif %} |
assert_template_result ' false ', text, 'array' => [1,2,3] expected = %| false |
assert_equal expected, Template.parse(text).render('array' => [1,2,3])
end end
def test_nil def test_nil
text = ' {% if var == nil %} true {% else %} false {% endif %} ' text = %| {% if var == nil %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'var' => nil expected = %| true |
assert_equal expected, Template.parse(text).render('var' => nil)
text = ' {% if var == null %} true {% else %} false {% endif %} ' text = %| {% if var == null %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'var' => nil expected = %| true |
assert_equal expected, Template.parse(text).render('var' => nil)
end end
def test_not_nil def test_not_nil
text = ' {% if var != nil %} true {% else %} false {% endif %} ' text = %| {% if var != nil %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'var' => 1 expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 1 )
text = ' {% if var != null %} true {% else %} false {% endif %} ' text = %| {% if var != null %} true {% else %} false {% endif %} |
assert_template_result ' true ', text, 'var' => 1 expected = %| true |
assert_equal expected, Template.parse(text).render('var' => 1 )
end end
end # StatementsTest end # StatementsTest

View File

@@ -27,57 +27,57 @@ class TemplateTest < Test::Unit::TestCase
def test_instance_assigns_persist_on_same_template_object_between_parses def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render
assert_equal 'from instance assigns', t.parse("{{ foo }}").render! assert_equal 'from instance assigns', t.parse("{{ foo }}").render
end end
def test_instance_assigns_persist_on_same_template_parsing_between_renders def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal 'foo', t.render! assert_equal 'foo', t.render
assert_equal 'foofoo', t.render! assert_equal 'foofoo', t.render
end end
def test_custom_assigns_do_not_persist_on_same_template def test_custom_assigns_do_not_persist_on_same_template
t = Template.new t = Template.new
assert_equal 'from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns') assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns')
assert_equal '', t.parse("{{ foo }}").render! assert_equal '', t.parse("{{ foo }}").render
end end
def test_custom_assigns_squash_instance_assigns def test_custom_assigns_squash_instance_assigns
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render
assert_equal 'from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns') assert_equal 'from custom assigns', t.parse("{{ foo }}").render('foo' => 'from custom assigns')
end end
def test_persistent_assigns_squash_instance_assigns def test_persistent_assigns_squash_instance_assigns
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render
t.assigns['foo'] = 'from persistent assigns' t.assigns['foo'] = 'from persistent assigns'
assert_equal 'from persistent assigns', t.parse("{{ foo }}").render! assert_equal 'from persistent assigns', t.parse("{{ foo }}").render
end end
def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
t.assigns['number'] = lambda { @global ||= 0; @global += 1 } t.assigns['number'] = lambda { @global ||= 0; @global += 1 }
assert_equal '1', t.parse("{{number}}").render! assert_equal '1', t.parse("{{number}}").render
assert_equal '1', t.parse("{{number}}").render! assert_equal '1', t.parse("{{number}}").render
assert_equal '1', t.render! assert_equal '1', t.render
@global = nil @global = nil
end end
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
assigns = {'number' => lambda { @global ||= 0; @global += 1 }} assigns = {'number' => lambda { @global ||= 0; @global += 1 }}
assert_equal '1', t.parse("{{number}}").render!(assigns) assert_equal '1', t.parse("{{number}}").render(assigns)
assert_equal '1', t.parse("{{number}}").render!(assigns) assert_equal '1', t.parse("{{number}}").render(assigns)
assert_equal '1', t.render!(assigns) assert_equal '1', t.render(assigns)
@global = nil @global = nil
end end
def test_resource_limits_works_with_custom_length_method def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}") t = Template.parse("{% assign foo = bar %}")
t.resource_limits = { :render_length_limit => 42 } t.resource_limits = { :render_length_limit => 42 }
assert_equal "", t.render!("bar" => SomethingWithLength.new) assert_equal "", t.render("bar" => SomethingWithLength.new)
end end
def test_resource_limits_render_length def test_resource_limits_render_length
@@ -86,7 +86,7 @@ class TemplateTest < Test::Unit::TestCase
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached] assert t.resource_limits[:reached]
t.resource_limits = { :render_length_limit => 10 } t.resource_limits = { :render_length_limit => 10 }
assert_equal "0123456789", t.render!() assert_equal "0123456789", t.render()
assert_not_nil t.resource_limits[:render_length_current] assert_not_nil t.resource_limits[:render_length_current]
end end
@@ -100,7 +100,7 @@ class TemplateTest < Test::Unit::TestCase
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached] assert t.resource_limits[:reached]
t.resource_limits = { :render_score_limit => 200 } t.resource_limits = { :render_score_limit => 200 }
assert_equal (" foo " * 100), t.render!() assert_equal (" foo " * 100), t.render()
assert_not_nil t.resource_limits[:render_score_current] assert_not_nil t.resource_limits[:render_score_current]
end end
@@ -110,7 +110,7 @@ class TemplateTest < Test::Unit::TestCase
assert_equal "Liquid error: Memory limits exceeded", t.render() assert_equal "Liquid error: Memory limits exceeded", t.render()
assert t.resource_limits[:reached] assert t.resource_limits[:reached]
t.resource_limits = { :assign_score_limit => 2 } t.resource_limits = { :assign_score_limit => 2 }
assert_equal "", t.render!() assert_equal "", t.render()
assert_not_nil t.resource_limits[:assign_score_current] assert_not_nil t.resource_limits[:assign_score_current]
end end
@@ -123,7 +123,7 @@ class TemplateTest < Test::Unit::TestCase
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")
t.render!() t.render()
assert t.resource_limits[:assign_score_current] > 0 assert t.resource_limits[:assign_score_current] > 0
assert t.resource_limits[:render_score_current] > 0 assert t.resource_limits[:render_score_current] > 0
assert t.resource_limits[:render_length_current] > 0 assert t.resource_limits[:render_length_current] > 0
@@ -133,9 +133,9 @@ class TemplateTest < Test::Unit::TestCase
t = Template.new t = Template.new
t.registers['lulz'] = 'haha' t.registers['lulz'] = 'haha'
drop = TemplateContextDrop.new drop = TemplateContextDrop.new
assert_equal 'fizzbuzz', t.parse('{{foo}}').render!(drop) assert_equal 'fizzbuzz', t.parse('{{foo}}').render(drop)
assert_equal 'bar', t.parse('{{bar}}').render!(drop) assert_equal 'bar', t.parse('{{bar}}').render(drop)
assert_equal 'haha', t.parse("{{baz}}").render!(drop) assert_equal 'haha', t.parse("{{baz}}").render(drop)
end end
def test_sets_default_localization_in_document def test_sets_default_localization_in_document

View File

@@ -21,8 +21,23 @@ class TokenizerTest < Test::Unit::TestCase
assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ") assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ")
end 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 private
def assert_tokens(source, expected)
assert_equal expected, tokenize(source)
assert_equal expected, old_tokenize(source)
end
def tokenize(source) def tokenize(source)
tokenizer = Liquid::Tokenizer.new(source) tokenizer = Liquid::Tokenizer.new(source)
tokens = [] tokens = []
@@ -31,4 +46,19 @@ class TokenizerTest < Test::Unit::TestCase
end end
tokens tokens
end 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 end

View File

@@ -141,49 +141,49 @@ class VariableResolutionTest < Test::Unit::TestCase
def test_simple_variable def test_simple_variable
template = Template.parse(%|{{test}}|) template = Template.parse(%|{{test}}|)
assert_equal 'worked', template.render!('test' => 'worked') assert_equal 'worked', template.render('test' => 'worked')
assert_equal 'worked wonderfully', template.render!('test' => 'worked wonderfully') assert_equal 'worked wonderfully', template.render('test' => 'worked wonderfully')
end end
def test_simple_with_whitespaces def test_simple_with_whitespaces
template = Template.parse(%| {{ test }} |) template = Template.parse(%| {{ test }} |)
assert_equal ' worked ', template.render!('test' => 'worked') assert_equal ' worked ', template.render('test' => 'worked')
assert_equal ' worked wonderfully ', template.render!('test' => 'worked wonderfully') assert_equal ' worked wonderfully ', template.render('test' => 'worked wonderfully')
end end
def test_ignore_unknown def test_ignore_unknown
template = Template.parse(%|{{ test }}|) template = Template.parse(%|{{ test }}|)
assert_equal '', template.render! assert_equal '', template.render
end end
def test_hash_scoping def test_hash_scoping
template = Template.parse(%|{{ test.test }}|) template = Template.parse(%|{{ test.test }}|)
assert_equal 'worked', template.render!('test' => {'test' => 'worked'}) assert_equal 'worked', template.render('test' => {'test' => 'worked'})
end end
def test_preset_assigns def test_preset_assigns
template = Template.parse(%|{{ test }}|) template = Template.parse(%|{{ test }}|)
template.assigns['test'] = 'worked' template.assigns['test'] = 'worked'
assert_equal 'worked', template.render! assert_equal 'worked', template.render
end end
def test_reuse_parsed_template def test_reuse_parsed_template
template = Template.parse(%|{{ greeting }} {{ name }}|) template = Template.parse(%|{{ greeting }} {{ name }}|)
template.assigns['greeting'] = 'Goodbye' template.assigns['greeting'] = 'Goodbye'
assert_equal 'Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi') assert_equal 'Hello Tobi', template.render('greeting' => 'Hello', 'name' => 'Tobi')
assert_equal 'Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi') assert_equal 'Hello ', template.render('greeting' => 'Hello', 'unknown' => 'Tobi')
assert_equal 'Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian') assert_equal 'Hello Brian', template.render('greeting' => 'Hello', 'name' => 'Brian')
assert_equal 'Goodbye Brian', template.render!('name' => 'Brian') assert_equal 'Goodbye Brian', template.render('name' => 'Brian')
assert_equal({'greeting'=>'Goodbye'}, template.assigns) assert_equal({'greeting'=>'Goodbye'}, template.assigns)
end end
def test_assigns_not_polluted_from_template def test_assigns_not_polluted_from_template
template = Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|) template = Template.parse(%|{{ test }}{% assign test = 'bar' %}{{ test }}|)
template.assigns['test'] = 'baz' template.assigns['test'] = 'baz'
assert_equal 'bazbar', template.render! assert_equal 'bazbar', template.render
assert_equal 'bazbar', template.render! assert_equal 'bazbar', template.render
assert_equal 'foobar', template.render!('test' => 'foo') assert_equal 'foobar', template.render('test' => 'foo')
assert_equal 'bazbar', template.render! assert_equal 'bazbar', template.render
end end
def test_hash_with_default_proc def test_hash_with_default_proc
@@ -197,8 +197,4 @@ class VariableResolutionTest < Test::Unit::TestCase
} }
assert_equal "Unknown variable 'test'", e.message assert_equal "Unknown variable 'test'", e.message
end end
def test_multiline_variable
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
end
end # VariableTest end # VariableTest

View File

@@ -26,13 +26,13 @@ module Test
include Liquid include Liquid
def assert_template_result(expected, template, assigns = {}, message = nil) def assert_template_result(expected, template, assigns = {}, message = nil)
assert_equal expected, Template.parse(template).render!(assigns) assert_equal expected, Template.parse(template).render(assigns)
end end
def assert_template_result_matches(expected, template, assigns = {}, message = nil) def assert_template_result_matches(expected, template, assigns = {}, message = nil)
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
assert_match expected, Template.parse(template).render!(assigns) assert_match expected, Template.parse(template).render(assigns)
end end
def assert_match_syntax_error(match, template, registers = {}) def assert_match_syntax_error(match, template, registers = {})