diff --git a/History.txt b/History.txt index 2f1c37f..d140a20 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,12 @@ +2.2.1 / 2010-08-23 + +* Added support for literal tags + +2.2.0 / 2010-08-22 + +* Compatible with Ruby 1.8.7, 1.9.1 and 1.9.2-p0 +* Merged some changed made by the community + 1.9.0 / 2008-03-04 * Fixed gem install rake task diff --git a/Manifest.txt b/Manifest.txt index 593e0bf..cbc3e89 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -2,7 +2,7 @@ CHANGELOG History.txt MIT-LICENSE Manifest.txt -README.txt +README.md Rakefile init.rb lib/extras/liquid_view.rb diff --git a/README.md b/README.md new file mode 100644 index 0000000..8480a29 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Liquid template engine + +## Introduction + +Liquid is a template engine which I wrote for very specific requirements + +* It has to have beautiful and simple markup. Template engines which don't produce good looking markup are no fun to use. +* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote. +* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects. + +## Why should I use Liquid + +* You want to allow your users to edit the appearance of your application but don't want them to run **insecure code on your server**. +* You want to render templates directly from the database +* You like smarty (PHP) style template engines +* You need a template engine which does HTML just as well as emails +* You don't like the markup of your current templating engine + +## What does it look like? + + + + + +## Howto use Liquid + +Liquid supports a very simple API based around the Liquid::Template class. +For standard use you can just pass it the content of a file and call render with a parameters hash. + +
+@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
+@template.render( 'name' => 'tobi' )              # => "hi tobi"
+
\ No newline at end of file diff --git a/README.txt b/README.txt deleted file mode 100644 index 1d019af..0000000 --- a/README.txt +++ /dev/null @@ -1,38 +0,0 @@ -= Liquid template engine - -Liquid is a template engine which I wrote for very specific requirements - -* It has to have beautiful and simple markup. - Template engines which don't produce good looking markup are no fun to use. -* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote. -* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can - just render it passing in a hash with local variables and objects. - -== Why should i use Liquid - -* You want to allow your users to edit the appearance of your application but don't want them to run insecure code on your server. -* You want to render templates directly from the database -* You like smarty style template engines -* You need a template engine which does HTML just as well as Emails -* You don't like the markup of your current one - -== What does it look like? - - - -== Howto use Liquid - -Liquid supports a very simple API based around the Liquid::Template class. -For standard use you can just pass it the content of a file and call render with a parameters hash. - - @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template - @template.render( 'name' => 'tobi' ) # => "hi tobi" \ No newline at end of file diff --git a/lib/liquid.rb b/lib/liquid.rb index dccef10..533bfb4 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -45,6 +45,7 @@ module Liquid PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/ TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/ VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/ + LiteralShorthand = /^(?:\{\{\{\s?)(.*?)(?:\s*\}\}\})$/ end require 'liquid/drop' diff --git a/lib/liquid/tag.rb b/lib/liquid/tag.rb index 2750064..b7f2aa4 100644 --- a/lib/liquid/tag.rb +++ b/lib/liquid/tag.rb @@ -1,6 +1,7 @@ module Liquid class Tag + attr_accessor :nodelist def initialize(tag_name, markup, tokens) @@ -19,8 +20,7 @@ module Liquid def render(context) '' end - end + end # Tag -end - +end # Tag diff --git a/lib/liquid/tags/comment.rb b/lib/liquid/tags/comment.rb index 8ce7e0e..37fb4c8 100644 --- a/lib/liquid/tags/comment.rb +++ b/lib/liquid/tags/comment.rb @@ -1,9 +1,9 @@ module Liquid - class Comment < Block + class Comment < Block def render(context) '' - end + end end - - Template.register_tag('comment', Comment) -end \ No newline at end of file + + Template.register_tag('comment', Comment) +end diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb index f060000..3b77732 100644 --- a/lib/liquid/tags/if.rb +++ b/lib/liquid/tags/if.rb @@ -14,17 +14,16 @@ module Liquid class If < Block SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]" Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/ - ExpressionsAndOperators = /(?:\b(?:and|or)\b|(?:\s*(?!\b(?:and|or)\b)(?:#{QuotedFragment}|\S+)\s*)+)/ - - def initialize(tag_name, markup, tokens) - + ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/ + + def initialize(tag_name, markup, tokens) @blocks = [] - + push_block('if', markup) - - super + + super end - + def unknown_tag(tag, markup, tokens) if ['elsif', 'else'].include?(tag) push_block(tag, markup) @@ -32,49 +31,49 @@ module Liquid super end end - + def render(context) context.stack do @blocks.each do |block| - if block.evaluate(context) - return render_all(block.attachment, context) + if block.evaluate(context) + return render_all(block.attachment, context) end - end + end '' end end - - private - - def push_block(tag, markup) - block = if tag == 'else' - ElseCondition.new - else - - expressions = markup.scan(ExpressionsAndOperators).reverse - raise(SyntaxError, SyntaxHelp) unless expressions.shift =~ Syntax - condition = Condition.new($1, $2, $3) - - while not expressions.empty? - operator = expressions.shift - - raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax - - new_condition = Condition.new($1, $2, $3) - new_condition.send(operator.to_sym, condition) - condition = new_condition - end - - condition + private + + def push_block(tag, markup) + block = if tag == 'else' + ElseCondition.new + else + + expressions = markup.scan(ExpressionsAndOperators).reverse + raise(SyntaxError, SyntaxHelp) unless expressions.shift =~ Syntax + + condition = Condition.new($1, $2, $3) + + while not expressions.empty? + operator = (expressions.shift).to_s.strip + + raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax + + new_condition = Condition.new($1, $2, $3) + new_condition.send(operator.to_sym, condition) + condition = new_condition + end + + condition + end + + @blocks.push(block) + @nodelist = block.attach(Array.new) end - - @blocks.push(block) - @nodelist = block.attach(Array.new) - end - - + + end Template.register_tag('if', If) -end \ No newline at end of file +end diff --git a/lib/liquid/tags/literal.rb b/lib/liquid/tags/literal.rb new file mode 100644 index 0000000..67f1600 --- /dev/null +++ b/lib/liquid/tags/literal.rb @@ -0,0 +1,42 @@ +module Liquid + + class Literal < Block + + # Class methods + + # Converts a shorthand Liquid literal into its long representation. + # + # Currently the Template parser only knows how to handle the long version. + # So, it always checks if it is in the presence of a literal, in which case it gets converted through this method. + # + # Example: + # Liquid::Literal "{{{ hello world }}}" #=> "{% literal %} hello world {% endliteral %}" + def self.from_shorthand(literal) + literal =~ LiteralShorthand ? "{% literal %}#{$1}{% endliteral %}" : literal + end + + # Public instance methods + + def parse(tokens) # :nodoc: + @nodelist ||= [] + @nodelist.clear + + while token = tokens.shift + if token =~ FullToken && block_delimiter == $1 + end_tag + return + else + @nodelist << token + end + end + + # Make sure that its ok to end parsing in the current block. + # Effectively this method will throw and exception unless the current block is + # of type Document + assert_missing_delimitation! + end # parse + + end + + Template.register_tag('literal', Literal) +end diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index edfb980..d346c44 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -55,7 +55,7 @@ module Liquid # Parse source code. # Returns self for easy chaining def parse(source) - @root = Document.new(tokenize(source)) + @root = Document.new(tokenize(Liquid::Literal.from_shorthand(source))) self end diff --git a/liquid.gemspec b/liquid.gemspec index 7d957f6..ed78917 100644 --- a/liquid.gemspec +++ b/liquid.gemspec @@ -1,16 +1,16 @@ Gem::Specification.new do |s| s.name = %q{liquid} - s.version = "2.2.0" + s.version = "2.2.1" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Tobias Luetke"] s.description = %q{A secure, non-evaling end user template engine with aesthetic markup.} s.email = %q{tobi@leetsoft.com} - s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"] - s.files = ["CHANGELOG", "History.txt", "MIT-LICENSE", "Manifest.txt", "README.txt", "Rakefile", "lib/extras/liquid_view.rb", "lib/liquid.rb", "lib/liquid/block.rb", "lib/liquid/condition.rb", "lib/liquid/context.rb", "lib/liquid/document.rb", "lib/liquid/drop.rb", "lib/liquid/errors.rb", "lib/liquid/extensions.rb", "lib/liquid/file_system.rb", "lib/liquid/htmltags.rb", "lib/liquid/module_ex.rb", "lib/liquid/standardfilters.rb", "lib/liquid/strainer.rb", "lib/liquid/tag.rb", "lib/liquid/tags/assign.rb", "lib/liquid/tags/capture.rb", "lib/liquid/tags/case.rb", "lib/liquid/tags/comment.rb", "lib/liquid/tags/cycle.rb", "lib/liquid/tags/for.rb", "lib/liquid/tags/if.rb", "lib/liquid/tags/ifchanged.rb", "lib/liquid/tags/include.rb", "lib/liquid/tags/unless.rb", "lib/liquid/template.rb", "lib/liquid/variable.rb"] + s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.md"] + s.files = ["CHANGELOG", "History.txt", "MIT-LICENSE", "Manifest.txt", "README.md", "Rakefile", "lib/extras/liquid_view.rb", "lib/liquid.rb", "lib/liquid/block.rb", "lib/liquid/condition.rb", "lib/liquid/context.rb", "lib/liquid/document.rb", "lib/liquid/drop.rb", "lib/liquid/errors.rb", "lib/liquid/extensions.rb", "lib/liquid/file_system.rb", "lib/liquid/htmltags.rb", "lib/liquid/module_ex.rb", "lib/liquid/standardfilters.rb", "lib/liquid/strainer.rb", "lib/liquid/tag.rb", "lib/liquid/tags/assign.rb", "lib/liquid/tags/capture.rb", "lib/liquid/tags/case.rb", "lib/liquid/tags/comment.rb", "lib/liquid/tags/cycle.rb", "lib/liquid/tags/for.rb", "lib/liquid/tags/if.rb", "lib/liquid/tags/ifchanged.rb", "lib/liquid/tags/include.rb", "lib/liquid/tags/unless.rb", "lib/liquid/template.rb", "lib/liquid/variable.rb"] s.has_rdoc = true s.homepage = %q{http://www.liquidmarkup.org} - s.rdoc_options = ["--main", "README.txt"] + s.rdoc_options = ["--main", "README.md"] s.require_paths = ["lib"] s.rubyforge_project = %q{liquid} s.rubygems_version = %q{1.3.1} diff --git a/test/lib/liquid/condition_test.rb b/test/lib/liquid/condition_test.rb index 5027798..7dd096e 100644 --- a/test/lib/liquid/condition_test.rb +++ b/test/lib/liquid/condition_test.rb @@ -48,16 +48,14 @@ class ConditionTest < Test::Unit::TestCase @context = Liquid::Context.new @context['array'] = [1,2,3,4,5] - assert_evalutes_false "array", 'contains', '0' - assert_evalutes_true "array", 'contains', '1' - assert_evalutes_true "array", 'contains', '2' - assert_evalutes_true "array", 'contains', '3' - assert_evalutes_true "array", 'contains', '4' - assert_evalutes_true "array", 'contains', '5' - assert_evalutes_false "array", 'contains', '6' - - assert_evalutes_false "array", 'contains', '"1"' - + assert_evalutes_false "array", 'contains', '0' + assert_evalutes_true "array", 'contains', '1' + assert_evalutes_true "array", 'contains', '2' + assert_evalutes_true "array", 'contains', '3' + assert_evalutes_true "array", 'contains', '4' + assert_evalutes_true "array", 'contains', '5' + assert_evalutes_false "array", 'contains', '6' + assert_evalutes_false "array", 'contains', '"1"' end def test_contains_returns_false_for_nil_operands @@ -94,17 +92,23 @@ class ConditionTest < Test::Unit::TestCase assert_equal false, condition.evaluate end - def test_should_allow_custom_proc_operator Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}} } - assert_evalutes_true "'bob'", 'starts_with', "'b'" - assert_evalutes_false "'bob'", 'starts_with', "'o'" + assert_evalutes_true "'bob'", 'starts_with', "'b'" + assert_evalutes_false "'bob'", 'starts_with', "'o'" ensure Condition.operators.delete 'starts_with' end + def test_left_or_right_may_contain_operators + @context = Liquid::Context.new + @context['one'] = @context['another'] = "gnomeslab-and-or-liquid" + + assert_evalutes_true "one", '==', "another" + end + private def assert_evalutes_true(left, op, right) assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), diff --git a/test/lib/liquid/regexp_test.rb b/test/lib/liquid/regexp_test.rb index 280d62b..1259d97 100644 --- a/test/lib/liquid/regexp_test.rb +++ b/test/lib/liquid/regexp_test.rb @@ -41,4 +41,9 @@ class RegexpTest < Test::Unit::TestCase assert_equal ['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser) assert_equal ['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser) end + + def test_literal_shorthand_regexp + assert_equal [["{% if 'gnomeslab' contains 'liquid' %}yes{% endif %}"]], + "{{{ {% if 'gnomeslab' contains 'liquid' %}yes{% endif %} }}}".scan(LiteralShorthand) + end end # RegexpTest diff --git a/test/lib/liquid/tags/if_else_test.rb b/test/lib/liquid/tags/if_else_test.rb index 2766290..cd5427b 100644 --- a/test/lib/liquid/tags/if_else_test.rb +++ b/test/lib/liquid/tags/if_else_test.rb @@ -150,4 +150,11 @@ class IfElseTest < Test::Unit::TestCase ensure Condition.operators.delete 'contains' end + + def test_operators_are_ignored_unless_isolated + Condition.operators['contains'] = :[] + + assert_template_result('yes', + %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) + end end # IfElseTest diff --git a/test/lib/liquid/tags/literal_test.rb b/test/lib/liquid/tags/literal_test.rb new file mode 100644 index 0000000..d5b970c --- /dev/null +++ b/test/lib/liquid/tags/literal_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class LiteralTagTest < Test::Unit::TestCase + include Liquid + + def test_empty_literal + assert_template_result '', '{% literal %}{% endliteral %}' + assert_template_result '', '{{{}}}' + end + + def test_simple_literal_value + assert_template_result 'howdy', + '{% literal %}howdy{% endliteral %}' + end + + def test_literals_ignore_liquid_markup + expected = %({% if 'gnomeslab' contain 'liquid' %}yes{ % endif %}) + template = %({% literal %}#{expected}{% endliteral %}) + + assert_template_result expected, template + end + + def test_shorthand_syntax + expected = %({% if 'gnomeslab' contain 'liquid' %}yes{ % endif %}) + template = %({{{#{expected}}}}) + + assert_template_result expected, template + end + + # Class methods + def test_from_shorthand + assert_equal '{% literal %}gnomeslab{% endliteral %}', Liquid::Literal.from_shorthand('{{{gnomeslab}}}') + end + + def test_from_shorthand_ignores_improper_syntax + text = "{% if 'hi' == 'hi' %}hi{% endif %}" + assert_equal text, Liquid::Literal.from_shorthand(text) + end +end # AssignTest \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index f64c301..32f96f5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,10 +2,12 @@ extras_path = File.join File.dirname(__FILE__), 'extra' $LOAD_PATH.unshift(extras_path) unless $LOAD_PATH.include? extras_path +require 'rubygems' unless RUBY_VERSION =~ /^(?:1.9.*)$/ require 'test/unit' require 'test/unit/assertions' require 'caller' require 'breakpoint' +require 'ruby-debug' require File.join File.dirname(__FILE__), '..', 'lib', 'liquid' @@ -16,9 +18,15 @@ module Test module Assertions 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) end + + def assert_template_result_matches(expected, template, assigns = {}, message = nil) + return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp + + assert_match expected, Template.parse(template).render(assigns) + end end # Assertions end # Unit