mirror of
https://github.com/kemko/liquid.git
synced 2026-01-01 15:55:40 +03:00
Merge branch 'master' of github.com:tobi/liquid
Conflicts: liquid.gemspec
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
42
README.md
Normal 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>
|
||||
38
README.txt
38
README.txt
@@ -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"
|
||||
@@ -45,6 +45,7 @@ module Liquid
|
||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/
|
||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/
|
||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/
|
||||
LiteralShorthand = /^(?:\{\{\{\s?)(.*?)(?:\s*\}\}\})$/
|
||||
end
|
||||
|
||||
require 'liquid/drop'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
42
lib/liquid/tags/literal.rb
Normal file
42
lib/liquid/tags/literal.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
39
test/lib/liquid/tags/literal_test.rb
Normal file
39
test/lib/liquid/tags/literal_test.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user