Merge branch 'master' of github.com:tobi/liquid

Conflicts:
	liquid.gemspec
This commit is contained in:
Tobias Lütke
2010-08-23 20:22:58 -04:00
16 changed files with 226 additions and 108 deletions

View File

@@ -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

View File

@@ -2,7 +2,7 @@ CHANGELOG
History.txt
MIT-LICENSE
Manifest.txt
README.txt
README.md
Rakefile
init.rb
lib/extras/liquid_view.rb

42
README.md Normal file
View File

@@ -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?
<code>
<ul id="products">
{% for product in products %}
<li>
<h2>{{product.name}}</h2>
Only {{product.price | price }}
{{product.description | prettyprint | paragraph }}
</li>
{% endfor %}
</ul>
</code>
## 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.
<pre>
@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
@template.render( 'name' => 'tobi' ) # => "hi tobi"
</pre>

View File

@@ -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?
<ul id="products">
{% for product in products %}
<li>
<h2>{{product.name}}</h2>
Only {{product.price | price }}
{{product.description | prettyprint | paragraph }}
</li>
{% endfor %}
</ul>
== 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"

View File

@@ -45,6 +45,7 @@ module Liquid
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/
LiteralShorthand = /^(?:\{\{\{\s?)(.*?)(?:\s*\}\}\})$/
end
require 'liquid/drop'

View File

@@ -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

View File

@@ -1,9 +1,9 @@
module Liquid
class Comment < Block
class Comment < Block
def render(context)
''
end
end
end
Template.register_tag('comment', Comment)
end
Template.register_tag('comment', Comment)
end

View File

@@ -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
end

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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),

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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