diff --git a/History.txt b/History.txt index 2f1c37f6..d140a206 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 593e0bfe..cbc3e890 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 00000000..8480a29e --- /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 1d019af6..00000000 --- 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 dccef10c..533bfb45 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 2750064f..b7f2aa45 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 8ce7e0eb..37fb4c8f 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 f0600009..3b777325 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 00000000..67f1600f --- /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 edfb980e..d346c44a 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 7d957f6f..ed789171 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 50277982..7dd096e8 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 280d62b0..1259d979 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 27662903..cd5427b2 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 00000000..d5b970c9 --- /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 f64c3013..32f96f5b 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