locomotive_liquid 2.1.3 → 2.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +19 -6
- data/README.md +39 -0
- data/Rakefile +31 -14
- data/lib/extras/liquid_view.rb +8 -8
- data/lib/liquid.rb +2 -1
- data/lib/liquid/context.rb +122 -127
- data/lib/liquid/errors.rb +1 -1
- data/lib/liquid/file_system.rb +10 -10
- data/lib/liquid/standardfilters.rb +59 -55
- data/lib/liquid/strainer.rb +3 -0
- data/lib/liquid/tags/comment.rb +4 -4
- data/lib/liquid/tags/if.rb +2 -2
- data/lib/liquid/tags/ifchanged.rb +8 -8
- data/lib/liquid/tags/literal.rb +42 -0
- data/lib/liquid/tags/unless.rb +9 -9
- data/lib/liquid/template.rb +1 -1
- metadata +10 -9
- data/README.txt +0 -38
data/History.txt
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
2.2.2
|
2
|
+
|
3
|
+
* Added support for template inheritance {% extends %}
|
4
|
+
|
5
|
+
2.2.1 / 2010-08-23
|
6
|
+
|
7
|
+
* Added support for literal tags
|
8
|
+
|
9
|
+
2.2.0 / 2010-08-22
|
10
|
+
|
11
|
+
* Compatible with Ruby 1.8.7, 1.9.1 and 1.9.2-p0
|
12
|
+
* Merged some changed made by the community
|
13
|
+
|
1
14
|
1.9.0 / 2008-03-04
|
2
15
|
|
3
16
|
* Fixed gem install rake task
|
@@ -7,7 +20,7 @@ Before 1.9.0
|
|
7
20
|
|
8
21
|
* Added If with or / and expressions
|
9
22
|
|
10
|
-
* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
|
23
|
+
* Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods.
|
11
24
|
|
12
25
|
* Added more tags to standard library
|
13
26
|
|
@@ -26,17 +39,17 @@ Before 1.9.0
|
|
26
39
|
* Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond]
|
27
40
|
|
28
41
|
* Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond]
|
29
|
-
|
30
|
-
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
|
31
|
-
|
32
42
|
|
33
|
-
|
43
|
+
{{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }}
|
44
|
+
|
45
|
+
|
46
|
+
* Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke]
|
34
47
|
|
35
48
|
class ProductDrop < Liquid::Drop
|
36
49
|
def top_sales
|
37
50
|
Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
|
38
51
|
end
|
39
|
-
end
|
52
|
+
end
|
40
53
|
t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' )
|
41
54
|
t.render('product' => ProductDrop.new )
|
42
55
|
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Liquid template engine
|
2
|
+
|
3
|
+
## Introduction
|
4
|
+
|
5
|
+
Liquid is a template engine which I wrote for very specific requirements
|
6
|
+
|
7
|
+
* It has to have beautiful and simple markup. Template engines which don't produce good looking markup are no fun to use.
|
8
|
+
* 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.
|
9
|
+
* 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.
|
10
|
+
|
11
|
+
## Why should I use Liquid
|
12
|
+
|
13
|
+
* 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**.
|
14
|
+
* You want to render templates directly from the database
|
15
|
+
* You like smarty (PHP) style template engines
|
16
|
+
* You need a template engine which does HTML just as well as emails
|
17
|
+
* You don't like the markup of your current templating engine
|
18
|
+
|
19
|
+
## What does it look like?
|
20
|
+
|
21
|
+
<ul id="products">
|
22
|
+
{% for product in products %}
|
23
|
+
<li>
|
24
|
+
<h2>{{product.name}}</h2>
|
25
|
+
Only {{product.price | price }}
|
26
|
+
|
27
|
+
{{product.description | prettyprint | paragraph }}
|
28
|
+
</li>
|
29
|
+
{% endfor %}
|
30
|
+
</ul>
|
31
|
+
|
32
|
+
## Howto use Liquid
|
33
|
+
|
34
|
+
Liquid supports a very simple API based around the Liquid::Template class.
|
35
|
+
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
36
|
+
|
37
|
+
|
38
|
+
@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
|
39
|
+
@template.render( 'name' => 'tobi' ) # => "hi tobi"
|
data/Rakefile
CHANGED
@@ -1,26 +1,37 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'rubygems'
|
3
|
+
|
4
|
+
require "bundler"
|
5
|
+
Bundler.setup
|
6
|
+
|
3
7
|
require 'rake'
|
4
|
-
require 'rake/testtask'
|
5
8
|
require 'rake/gempackagetask'
|
6
9
|
|
7
|
-
|
10
|
+
require "rspec"
|
11
|
+
require "rspec/core/rake_task"
|
12
|
+
|
13
|
+
Rspec::Core::RakeTask.new("spec") do |spec|
|
14
|
+
spec.pattern = "spec/**/*_spec.rb"
|
15
|
+
end
|
8
16
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
t.pattern = 'test/*_test.rb'
|
13
|
-
t.verbose = false
|
17
|
+
desc "Run the Integration Specs (rendering)"
|
18
|
+
Rspec::Core::RakeTask.new("spec:integration") do |spec|
|
19
|
+
spec.pattern = "spec/unit/*_spec.rb"
|
14
20
|
end
|
15
21
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
t.test_files = ['test/test_helper.rb', 'test/extends_test.rb', 'test/inherited_block_test.rb']
|
20
|
-
# t.test_files = ['test/test_helper.rb', 'test/inherited_block_test.rb', 'test/inherited_block_test.rb']
|
21
|
-
t.verbose = false
|
22
|
+
desc "Run the Unit Specs"
|
23
|
+
Rspec::Core::RakeTask.new("spec:unit") do |spec|
|
24
|
+
spec.pattern = "spec/unit/*_spec.rb"
|
22
25
|
end
|
23
26
|
|
27
|
+
desc "Run all the specs without all the verbose spec output"
|
28
|
+
Rspec::Core::RakeTask.new('spec:progress') do |spec|
|
29
|
+
spec.rspec_opts = %w(--format progress)
|
30
|
+
spec.pattern = "spec/**/*_spec.rb"
|
31
|
+
end
|
32
|
+
|
33
|
+
task :default => :spec
|
34
|
+
|
24
35
|
gemspec = eval(File.read('locomotive_liquid.gemspec'))
|
25
36
|
Rake::GemPackageTask.new(gemspec) do |pkg|
|
26
37
|
pkg.gem_spec = gemspec
|
@@ -28,7 +39,13 @@ end
|
|
28
39
|
|
29
40
|
desc "build the gem and release it to rubygems.org"
|
30
41
|
task :release => :gem do
|
31
|
-
|
42
|
+
puts "Tagging #{gemspec.version}..."
|
43
|
+
require 'ruby-debug';debugger
|
44
|
+
system "git tag -a #{gemspec.version} -m 'Tagging #{gemspec.version}'"
|
45
|
+
puts "Pushing to Github..."
|
46
|
+
system "git push --tags"
|
47
|
+
puts "Pushing to rubygems.org..."
|
48
|
+
system "gem push pkg/locomotive_liquid-#{gemspec.version}.gem"
|
32
49
|
end
|
33
50
|
|
34
51
|
namespace :profile do
|
data/lib/extras/liquid_view.rb
CHANGED
@@ -2,15 +2,15 @@
|
|
2
2
|
# and use liquid as an template system for .liquid files
|
3
3
|
#
|
4
4
|
# Example
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# ActionView::Base::register_template_handler :liquid, LiquidView
|
7
7
|
class LiquidView
|
8
8
|
PROTECTED_ASSIGNS = %w( template_root response _session template_class action_name request_origin session template
|
9
9
|
_response url _request _cookies variables_added _flash params _headers request cookies
|
10
10
|
ignore_missing_templates flash _params logger before_filter_chain_aborted headers )
|
11
|
-
PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
|
11
|
+
PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
|
12
12
|
@helpers @assigns_added @template @_render_stack @template_format @assigns )
|
13
|
-
|
13
|
+
|
14
14
|
def self.call(template)
|
15
15
|
"LiquidView.new(self).render(template, local_assigns)"
|
16
16
|
end
|
@@ -18,10 +18,10 @@ class LiquidView
|
|
18
18
|
def initialize(view)
|
19
19
|
@view = view
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
def render(template, local_assigns = nil)
|
23
23
|
@view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
|
24
|
-
|
24
|
+
|
25
25
|
# Rails 2.2 Template has source, but not locals
|
26
26
|
if template.respond_to?(:source) && !template.respond_to?(:locals)
|
27
27
|
assigns = (@view.instance_variables - PROTECTED_INSTANCE_VARIABLES).inject({}) do |hash, ivar|
|
@@ -31,15 +31,15 @@ class LiquidView
|
|
31
31
|
else
|
32
32
|
assigns = @view.assigns.reject{ |k,v| PROTECTED_ASSIGNS.include?(k) }
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
source = template.respond_to?(:source) ? template.source : template
|
36
36
|
local_assigns = (template.respond_to?(:locals) ? template.locals : local_assigns) || {}
|
37
|
-
|
37
|
+
|
38
38
|
if content_for_layout = @view.instance_variable_get("@content_for_layout")
|
39
39
|
assigns['content_for_layout'] = content_for_layout
|
40
40
|
end
|
41
41
|
assigns.merge!(local_assigns.stringify_keys)
|
42
|
-
|
42
|
+
|
43
43
|
liquid = Liquid::Template.parse(source)
|
44
44
|
liquid.render(assigns, :filters => [@view.controller.master_helper_module], :registers => {:action_view => @view, :controller => @view.controller})
|
45
45
|
end
|
data/lib/liquid.rb
CHANGED
@@ -38,13 +38,14 @@ module Liquid
|
|
38
38
|
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s,\|,\:,\,]+/
|
39
39
|
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/
|
40
40
|
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/
|
41
|
-
SpacelessFilter =
|
41
|
+
SpacelessFilter = /^(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/
|
42
42
|
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/
|
43
43
|
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/
|
44
44
|
AnyStartingTag = /\{\{|\{\%/
|
45
45
|
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/
|
46
46
|
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/
|
47
47
|
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/
|
48
|
+
LiteralShorthand = /^(?:\{\{\{\s?)(.*?)(?:\s*\}\}\})$/
|
48
49
|
end
|
49
50
|
|
50
51
|
require 'liquid/drop'
|
data/lib/liquid/context.rb
CHANGED
@@ -28,8 +28,9 @@ module Liquid
|
|
28
28
|
@strainer ||= Strainer.create(self)
|
29
29
|
end
|
30
30
|
|
31
|
-
#
|
32
|
-
#
|
31
|
+
# Adds filters to this context.
|
32
|
+
#
|
33
|
+
# Note that this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
|
33
34
|
# for that
|
34
35
|
def add_filters(filters)
|
35
36
|
filters = [filters].flatten.compact
|
@@ -52,7 +53,6 @@ module Liquid
|
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
55
|
-
|
56
56
|
def invoke(method, *args)
|
57
57
|
if strainer.respond_to?(method)
|
58
58
|
strainer.__send__(method, *args)
|
@@ -61,43 +61,44 @@ module Liquid
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
#
|
64
|
+
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
65
65
|
def push(new_scope={})
|
66
66
|
raise StackLevelError, "Nesting too deep" if @scopes.length > 100
|
67
67
|
@scopes.unshift(new_scope)
|
68
68
|
end
|
69
69
|
|
70
|
-
#
|
70
|
+
# Merge a hash of variables in the current local scope
|
71
71
|
def merge(new_scopes)
|
72
72
|
@scopes[0].merge!(new_scopes)
|
73
73
|
end
|
74
74
|
|
75
|
-
#
|
75
|
+
# Pop from the stack. use <tt>Context#stack</tt> instead
|
76
76
|
def pop
|
77
77
|
raise ContextError if @scopes.size == 1
|
78
78
|
@scopes.shift
|
79
79
|
end
|
80
80
|
|
81
|
-
#
|
81
|
+
# Pushes a new local scope on the stack, pops it at the end of the block
|
82
82
|
#
|
83
83
|
# Example:
|
84
|
-
#
|
85
84
|
# context.stack do
|
86
85
|
# context['var'] = 'hi'
|
87
86
|
# end
|
88
|
-
# context['var] #=> nil
|
89
87
|
#
|
88
|
+
# context['var] #=> nil
|
90
89
|
def stack(new_scope={},&block)
|
91
90
|
result = nil
|
92
91
|
push(new_scope)
|
92
|
+
|
93
93
|
begin
|
94
94
|
result = yield
|
95
95
|
ensure
|
96
96
|
pop
|
97
97
|
end
|
98
|
+
|
98
99
|
result
|
99
100
|
end
|
100
|
-
|
101
|
+
|
101
102
|
def clear_instance_assigns
|
102
103
|
@scopes[0] = {}
|
103
104
|
end
|
@@ -116,139 +117,133 @@ module Liquid
|
|
116
117
|
end
|
117
118
|
|
118
119
|
private
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
(resolve($1).to_i..resolve($2).to_i)
|
153
|
-
# Floats
|
154
|
-
when /^(\d[\d\.]+)$/
|
155
|
-
$1.to_f
|
156
|
-
else
|
157
|
-
variable(key)
|
120
|
+
# Look up variable, either resolve directly after considering the name. We can directly handle
|
121
|
+
# Strings, digits, floats and booleans (true,false).
|
122
|
+
# If no match is made we lookup the variable in the current scope and
|
123
|
+
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
124
|
+
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
125
|
+
#
|
126
|
+
# Example:
|
127
|
+
# products == empty #=> products.empty?
|
128
|
+
def resolve(key)
|
129
|
+
case key
|
130
|
+
when nil, 'nil', 'null', ''
|
131
|
+
nil
|
132
|
+
when 'true'
|
133
|
+
true
|
134
|
+
when 'false'
|
135
|
+
false
|
136
|
+
when 'blank'
|
137
|
+
:blank?
|
138
|
+
when 'empty' # Single quoted strings
|
139
|
+
:empty?
|
140
|
+
when /^'(.*)'$/ # Double quoted strings
|
141
|
+
$1.to_s
|
142
|
+
when /^"(.*)"$/ # Integer and floats
|
143
|
+
$1.to_s
|
144
|
+
when /^(\d+)$/ # Ranges
|
145
|
+
$1.to_i
|
146
|
+
when /^\((\S+)\.\.(\S+)\)$/ # Floats
|
147
|
+
(resolve($1).to_i..resolve($2).to_i)
|
148
|
+
when /^(\d[\d\.]+)$/
|
149
|
+
$1.to_f
|
150
|
+
else
|
151
|
+
variable(key)
|
152
|
+
end
|
158
153
|
end
|
159
|
-
end
|
160
154
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
155
|
+
# Fetches an object starting at the local scope and then moving up the hierachy
|
156
|
+
def find_variable(key)
|
157
|
+
scope = @scopes.find { |s| s.has_key?(key) }
|
158
|
+
|
159
|
+
if scope.nil?
|
160
|
+
@environments.each do |e|
|
161
|
+
if variable = lookup_and_evaluate(e, key)
|
162
|
+
scope = e
|
163
|
+
break
|
164
|
+
end
|
170
165
|
end
|
171
166
|
end
|
172
|
-
end
|
173
|
-
scope ||= @environments.last || @scopes.last
|
174
|
-
variable ||= lookup_and_evaluate(scope, key)
|
175
|
-
|
176
|
-
variable = variable.to_liquid
|
177
|
-
variable.context = self if variable.respond_to?(:context=)
|
178
|
-
return variable
|
179
|
-
end
|
180
167
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
# assert_equal 'tobi', @context['hash.name']
|
187
|
-
# assert_equal 'tobi', @context['hash["name"]']
|
188
|
-
#
|
189
|
-
def variable(markup)
|
190
|
-
parts = markup.scan(VariableParser)
|
191
|
-
square_bracketed = /^\[(.*)\]$/
|
168
|
+
scope ||= @environments.last || @scopes.last
|
169
|
+
variable ||= lookup_and_evaluate(scope, key)
|
170
|
+
|
171
|
+
variable = variable.to_liquid
|
172
|
+
variable.context = self if variable.respond_to?(:context=)
|
192
173
|
|
193
|
-
|
194
|
-
if first_part =~ square_bracketed
|
195
|
-
first_part = resolve($1)
|
174
|
+
return variable
|
196
175
|
end
|
197
176
|
|
198
|
-
|
177
|
+
# Resolves namespaced queries gracefully.
|
178
|
+
#
|
179
|
+
# Example
|
180
|
+
# @context['hash'] = {"name" => 'tobi'}
|
181
|
+
# assert_equal 'tobi', @context['hash.name']
|
182
|
+
# assert_equal 'tobi', @context['hash["name"]']
|
183
|
+
def variable(markup)
|
184
|
+
parts = markup.scan(VariableParser)
|
185
|
+
square_bracketed = /^\[(.*)\]$/
|
199
186
|
|
200
|
-
parts.
|
201
|
-
part = resolve($1) if part_resolved = (part =~ square_bracketed)
|
187
|
+
first_part = parts.shift
|
202
188
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
((object.respond_to?(:has_key?) and object.has_key?(part)) or
|
207
|
-
(object.respond_to?(:fetch) and part.is_a?(Integer)))
|
189
|
+
if first_part =~ square_bracketed
|
190
|
+
first_part = resolve($1)
|
191
|
+
end
|
208
192
|
|
209
|
-
|
210
|
-
res = lookup_and_evaluate(object, part)
|
211
|
-
object = res.to_liquid
|
193
|
+
if object = find_variable(first_part)
|
212
194
|
|
213
|
-
|
214
|
-
|
215
|
-
# as commands and call them on the current object
|
216
|
-
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
|
195
|
+
parts.each do |part|
|
196
|
+
part = resolve($1) if part_resolved = (part =~ square_bracketed)
|
217
197
|
|
218
|
-
object
|
198
|
+
# If object is a hash- or array-like object we look for the
|
199
|
+
# presence of the key and if its available we return it
|
200
|
+
if object.respond_to?(:[]) and
|
201
|
+
((object.respond_to?(:has_key?) and object.has_key?(part)) or
|
202
|
+
(object.respond_to?(:fetch) and part.is_a?(Integer)))
|
219
203
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
return nil
|
224
|
-
end
|
204
|
+
# if its a proc we will replace the entry with the proc
|
205
|
+
res = lookup_and_evaluate(object, part)
|
206
|
+
object = res.to_liquid
|
225
207
|
|
226
|
-
|
227
|
-
|
208
|
+
# Some special cases. If the part wasn't in square brackets and
|
209
|
+
# no key with the same name was found we interpret following calls
|
210
|
+
# as commands and call them on the current object
|
211
|
+
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
|
212
|
+
|
213
|
+
object = object.send(part.intern).to_liquid
|
214
|
+
|
215
|
+
# No key was present with the desired value and it wasn't one of the directly supported
|
216
|
+
# keywords either. The only thing we got left is to return nil
|
217
|
+
else
|
218
|
+
return nil
|
219
|
+
end
|
220
|
+
|
221
|
+
# If we are dealing with a drop here we have to
|
222
|
+
object.context = self if object.respond_to?(:context=)
|
223
|
+
end
|
228
224
|
end
|
229
|
-
end
|
230
225
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
226
|
+
object
|
227
|
+
end # variable
|
228
|
+
|
229
|
+
def lookup_and_evaluate(obj, key)
|
230
|
+
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
231
|
+
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
232
|
+
else
|
233
|
+
value
|
234
|
+
end
|
235
|
+
end # lookup_and_evaluate
|
236
|
+
|
237
|
+
def squash_instance_assigns_with_environments
|
238
|
+
@scopes.last.each_key do |k|
|
239
|
+
@environments.each do |env|
|
240
|
+
if env.has_key?(k)
|
241
|
+
scopes.last[k] = lookup_and_evaluate(env, k)
|
242
|
+
break
|
243
|
+
end
|
248
244
|
end
|
249
245
|
end
|
250
|
-
end
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
end
|
246
|
+
end # squash_instance_assigns_with_environments
|
247
|
+
end # Context
|
248
|
+
|
249
|
+
end # Liquid
|
data/lib/liquid/errors.rb
CHANGED
data/lib/liquid/file_system.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Liquid
|
2
2
|
# A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
|
3
3
|
#
|
4
|
-
# You can implement subclasses that retrieve templates from the database, from the file system using a different
|
4
|
+
# You can implement subclasses that retrieve templates from the database, from the file system using a different
|
5
5
|
# path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
|
6
6
|
#
|
7
7
|
# You can add additional instance variables, arguments, or methods as needed.
|
@@ -18,7 +18,7 @@ module Liquid
|
|
18
18
|
raise FileSystemError, "This liquid context does not allow includes."
|
19
19
|
end
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
# This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
|
23
23
|
# ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
|
24
24
|
#
|
@@ -27,35 +27,35 @@ module Liquid
|
|
27
27
|
# Example:
|
28
28
|
#
|
29
29
|
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
30
|
-
#
|
30
|
+
#
|
31
31
|
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
32
32
|
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
33
33
|
#
|
34
34
|
class LocalFileSystem
|
35
35
|
attr_accessor :root
|
36
|
-
|
36
|
+
|
37
37
|
def initialize(root)
|
38
38
|
@root = root
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
def read_template_file(template_path)
|
42
42
|
full_path = full_path(template_path)
|
43
43
|
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
|
44
|
-
|
44
|
+
|
45
45
|
File.read(full_path)
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
def full_path(template_path)
|
49
49
|
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
|
50
|
-
|
50
|
+
|
51
51
|
full_path = if template_path.include?('/')
|
52
52
|
File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
|
53
53
|
else
|
54
54
|
File.join(root, "_#{template_path}.liquid")
|
55
55
|
end
|
56
|
-
|
56
|
+
|
57
57
|
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
|
58
|
-
|
58
|
+
|
59
59
|
full_path
|
60
60
|
end
|
61
61
|
end
|
@@ -1,36 +1,36 @@
|
|
1
1
|
require 'cgi'
|
2
2
|
|
3
3
|
module Liquid
|
4
|
-
|
4
|
+
|
5
5
|
module StandardFilters
|
6
|
-
|
6
|
+
|
7
7
|
# Return the size of an array or of an string
|
8
8
|
def size(input)
|
9
|
-
|
9
|
+
|
10
10
|
input.respond_to?(:size) ? input.size : 0
|
11
|
-
end
|
12
|
-
|
11
|
+
end
|
12
|
+
|
13
13
|
# convert a input string to DOWNCASE
|
14
14
|
def downcase(input)
|
15
15
|
input.to_s.downcase
|
16
|
-
end
|
16
|
+
end
|
17
17
|
|
18
18
|
# convert a input string to UPCASE
|
19
19
|
def upcase(input)
|
20
20
|
input.to_s.upcase
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
# capitalize words in the input centence
|
24
24
|
def capitalize(input)
|
25
25
|
input.to_s.capitalize
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def escape(input)
|
29
29
|
CGI.escapeHTML(input) rescue input
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
alias_method :h, :escape
|
33
|
-
|
33
|
+
|
34
34
|
# Truncate a string down to x characters
|
35
35
|
def truncate(input, length = 50, truncate_string = "...")
|
36
36
|
if input.nil? then return end
|
@@ -44,22 +44,26 @@ module Liquid
|
|
44
44
|
wordlist = input.to_s.split
|
45
45
|
l = words.to_i - 1
|
46
46
|
l = 0 if l < 0
|
47
|
-
wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
|
47
|
+
wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
def strip_html(input)
|
51
51
|
input.to_s.gsub(/<script.*?<\/script>/, '').gsub(/<.*?>/, '')
|
52
|
-
end
|
53
|
-
|
52
|
+
end
|
53
|
+
|
54
54
|
# Remove all newlines from the string
|
55
|
-
def strip_newlines(input)
|
56
|
-
input.to_s.gsub(/\n/, '')
|
55
|
+
def strip_newlines(input)
|
56
|
+
input.to_s.gsub(/\n/, '')
|
57
57
|
end
|
58
|
-
|
59
|
-
|
58
|
+
|
60
59
|
# Join elements of the array with certain character between them
|
61
|
-
def join(input,
|
62
|
-
|
60
|
+
def join(input, array_glue = ' ', hash_glue = nil)
|
61
|
+
hash_glue ||= array_glue
|
62
|
+
|
63
|
+
# translate from hash to array if needed
|
64
|
+
input = input.map{|k,v| "#{k}#{hash_glue}#{v}" } if input.is_a?(Hash)
|
65
|
+
|
66
|
+
[input].flatten.join(array_glue)
|
63
67
|
end
|
64
68
|
|
65
69
|
# Sort elements of the array
|
@@ -73,8 +77,8 @@ module Liquid
|
|
73
77
|
elsif ary.first.respond_to?(property)
|
74
78
|
ary.sort {|a,b| a.send(property) <=> b.send(property) }
|
75
79
|
end
|
76
|
-
end
|
77
|
-
|
80
|
+
end
|
81
|
+
|
78
82
|
# map/collect on a given property
|
79
83
|
def map(input, property)
|
80
84
|
ary = [input].flatten
|
@@ -84,42 +88,42 @@ module Liquid
|
|
84
88
|
ary.map {|e| e.send(property) }
|
85
89
|
end
|
86
90
|
end
|
87
|
-
|
91
|
+
|
88
92
|
# Replace occurrences of a string with another
|
89
93
|
def replace(input, string, replacement = '')
|
90
94
|
input.to_s.gsub(string, replacement)
|
91
95
|
end
|
92
|
-
|
96
|
+
|
93
97
|
# Replace the first occurrences of a string with another
|
94
98
|
def replace_first(input, string, replacement = '')
|
95
99
|
input.to_s.sub(string, replacement)
|
96
|
-
end
|
97
|
-
|
100
|
+
end
|
101
|
+
|
98
102
|
# remove a substring
|
99
103
|
def remove(input, string)
|
100
|
-
input.to_s.gsub(string, '')
|
104
|
+
input.to_s.gsub(string, '')
|
101
105
|
end
|
102
|
-
|
106
|
+
|
103
107
|
# remove the first occurrences of a substring
|
104
108
|
def remove_first(input, string)
|
105
|
-
input.to_s.sub(string, '')
|
106
|
-
end
|
107
|
-
|
109
|
+
input.to_s.sub(string, '')
|
110
|
+
end
|
111
|
+
|
108
112
|
# add one string to another
|
109
113
|
def append(input, string)
|
110
114
|
input.to_s + string.to_s
|
111
115
|
end
|
112
|
-
|
116
|
+
|
113
117
|
# prepend a string to another
|
114
118
|
def prepend(input, string)
|
115
119
|
string.to_s + input.to_s
|
116
120
|
end
|
117
|
-
|
121
|
+
|
118
122
|
# Add <br /> tags in front of all newlines in input string
|
119
|
-
def newline_to_br(input)
|
120
|
-
input.to_s.gsub(/\n/, "<br />\n")
|
123
|
+
def newline_to_br(input)
|
124
|
+
input.to_s.gsub(/\n/, "<br />\n")
|
121
125
|
end
|
122
|
-
|
126
|
+
|
123
127
|
# Reformat a date
|
124
128
|
#
|
125
129
|
# %a - The abbreviated weekday name (``Sun'')
|
@@ -149,62 +153,62 @@ module Liquid
|
|
149
153
|
# %Z - Time zone name
|
150
154
|
# %% - Literal ``%'' character
|
151
155
|
def date(input, format)
|
152
|
-
|
156
|
+
|
153
157
|
if format.to_s.empty?
|
154
158
|
return input.to_s
|
155
159
|
end
|
156
|
-
|
160
|
+
|
157
161
|
date = input.is_a?(String) ? Time.parse(input) : input
|
158
|
-
|
162
|
+
|
159
163
|
if date.respond_to?(:strftime)
|
160
164
|
date.strftime(format.to_s)
|
161
165
|
else
|
162
166
|
input
|
163
167
|
end
|
164
|
-
rescue => e
|
168
|
+
rescue => e
|
165
169
|
input
|
166
170
|
end
|
167
|
-
|
168
|
-
# Get the first element of the passed in array
|
169
|
-
#
|
171
|
+
|
172
|
+
# Get the first element of the passed in array
|
173
|
+
#
|
170
174
|
# Example:
|
171
175
|
# {{ product.images | first | to_img }}
|
172
|
-
#
|
176
|
+
#
|
173
177
|
def first(array)
|
174
178
|
array.first if array.respond_to?(:first)
|
175
179
|
end
|
176
180
|
|
177
|
-
# Get the last element of the passed in array
|
178
|
-
#
|
181
|
+
# Get the last element of the passed in array
|
182
|
+
#
|
179
183
|
# Example:
|
180
184
|
# {{ product.images | last | to_img }}
|
181
|
-
#
|
185
|
+
#
|
182
186
|
def last(array)
|
183
187
|
array.last if array.respond_to?(:last)
|
184
188
|
end
|
185
|
-
|
189
|
+
|
186
190
|
# addition
|
187
191
|
def plus(input, operand)
|
188
192
|
to_number(input) + to_number(operand)
|
189
193
|
end
|
190
|
-
|
194
|
+
|
191
195
|
# subtraction
|
192
196
|
def minus(input, operand)
|
193
197
|
to_number(input) - to_number(operand)
|
194
198
|
end
|
195
|
-
|
199
|
+
|
196
200
|
# multiplication
|
197
201
|
def times(input, operand)
|
198
202
|
to_number(input) * to_number(operand)
|
199
203
|
end
|
200
|
-
|
204
|
+
|
201
205
|
# division
|
202
206
|
def divided_by(input, operand)
|
203
207
|
to_number(input) / to_number(operand)
|
204
208
|
end
|
205
|
-
|
209
|
+
|
206
210
|
private
|
207
|
-
|
211
|
+
|
208
212
|
def to_number(obj)
|
209
213
|
case obj
|
210
214
|
when Numeric
|
@@ -215,8 +219,8 @@ module Liquid
|
|
215
219
|
0
|
216
220
|
end
|
217
221
|
end
|
218
|
-
|
222
|
+
|
219
223
|
end
|
220
|
-
|
224
|
+
|
221
225
|
Template.register_filter(StandardFilters)
|
222
226
|
end
|
data/lib/liquid/strainer.rb
CHANGED
@@ -16,6 +16,9 @@ module Liquid
|
|
16
16
|
INTERNAL_METHOD = /^__/
|
17
17
|
@@required_methods = Set.new([:__id__, :__send__, :respond_to?, :extend, :methods, :class, :object_id])
|
18
18
|
|
19
|
+
# Ruby 1.9.2 introduces Object#respond_to_missing?, which is invoked by Object#respond_to?
|
20
|
+
@@required_methods << :respond_to_missing? if Object.respond_to? :respond_to_missing?
|
21
|
+
|
19
22
|
@@filters = {}
|
20
23
|
|
21
24
|
def initialize(context)
|
data/lib/liquid/tags/comment.rb
CHANGED
data/lib/liquid/tags/if.rb
CHANGED
@@ -14,7 +14,7 @@ module Liquid
|
|
14
14
|
class If < Block
|
15
15
|
SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
16
16
|
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/
|
17
|
-
ExpressionsAndOperators = /(?:\b(
|
17
|
+
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/
|
18
18
|
|
19
19
|
def initialize(tag_name, markup, tokens, context)
|
20
20
|
|
@@ -57,7 +57,7 @@ module Liquid
|
|
57
57
|
condition = Condition.new($1, $2, $3)
|
58
58
|
|
59
59
|
while not expressions.empty?
|
60
|
-
operator = expressions.shift
|
60
|
+
operator = (expressions.shift).to_s.strip
|
61
61
|
|
62
62
|
raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax
|
63
63
|
|
@@ -1,20 +1,20 @@
|
|
1
1
|
module Liquid
|
2
2
|
class Ifchanged < Block
|
3
|
-
|
3
|
+
|
4
4
|
def render(context)
|
5
|
-
context.stack do
|
6
|
-
|
5
|
+
context.stack do
|
6
|
+
|
7
7
|
output = render_all(@nodelist, context)
|
8
|
-
|
8
|
+
|
9
9
|
if output != context.registers[:ifchanged]
|
10
10
|
context.registers[:ifchanged] = output
|
11
11
|
output
|
12
12
|
else
|
13
13
|
''
|
14
|
-
end
|
14
|
+
end
|
15
15
|
end
|
16
16
|
end
|
17
|
-
end
|
18
|
-
|
19
|
-
Template.register_tag('ifchanged', Ifchanged)
|
17
|
+
end
|
18
|
+
|
19
|
+
Template.register_tag('ifchanged', Ifchanged)
|
20
20
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
class Literal < Block
|
4
|
+
|
5
|
+
# Class methods
|
6
|
+
|
7
|
+
# Converts a shorthand Liquid literal into its long representation.
|
8
|
+
#
|
9
|
+
# Currently the Template parser only knows how to handle the long version.
|
10
|
+
# So, it always checks if it is in the presence of a literal, in which case it gets converted through this method.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# Liquid::Literal "{{{ hello world }}}" #=> "{% literal %} hello world {% endliteral %}"
|
14
|
+
def self.from_shorthand(literal)
|
15
|
+
literal =~ LiteralShorthand ? "{% literal %}#{$1}{% endliteral %}" : literal
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public instance methods
|
19
|
+
|
20
|
+
def parse(tokens) # :nodoc:
|
21
|
+
@nodelist ||= []
|
22
|
+
@nodelist.clear
|
23
|
+
|
24
|
+
while token = tokens.shift
|
25
|
+
if token =~ FullToken && block_delimiter == $1
|
26
|
+
end_tag
|
27
|
+
return
|
28
|
+
else
|
29
|
+
@nodelist << token
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Make sure that its ok to end parsing in the current block.
|
34
|
+
# Effectively this method will throw and exception unless the current block is
|
35
|
+
# of type Document
|
36
|
+
assert_missing_delimitation!
|
37
|
+
end # parse
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
Template.register_tag('literal', Literal)
|
42
|
+
end
|
data/lib/liquid/tags/unless.rb
CHANGED
@@ -9,25 +9,25 @@ module Liquid
|
|
9
9
|
class Unless < If
|
10
10
|
def render(context)
|
11
11
|
context.stack do
|
12
|
-
|
12
|
+
|
13
13
|
# First condition is interpreted backwards ( if not )
|
14
14
|
block = @blocks.first
|
15
15
|
unless block.evaluate(context)
|
16
|
-
return render_all(block.attachment, context)
|
16
|
+
return render_all(block.attachment, context)
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
# After the first condition unless works just like if
|
20
20
|
@blocks[1..-1].each do |block|
|
21
|
-
if block.evaluate(context)
|
22
|
-
return render_all(block.attachment, context)
|
21
|
+
if block.evaluate(context)
|
22
|
+
return render_all(block.attachment, context)
|
23
23
|
end
|
24
|
-
end
|
25
|
-
|
24
|
+
end
|
25
|
+
|
26
26
|
''
|
27
27
|
end
|
28
|
-
end
|
28
|
+
end
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
|
32
32
|
Template.register_tag('unless', Unless)
|
33
33
|
end
|
data/lib/liquid/template.rb
CHANGED
@@ -55,7 +55,7 @@ module Liquid
|
|
55
55
|
# Parse source code.
|
56
56
|
# Returns self for easy chaining
|
57
57
|
def parse(source, context = {})
|
58
|
-
@root = Document.new(tokenize(source), context.merge!(:template => self))
|
58
|
+
@root = Document.new(tokenize(Liquid::Literal.from_shorthand(source)), context.merge!(:template => self))
|
59
59
|
self
|
60
60
|
end
|
61
61
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: locomotive_liquid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 3
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 2.
|
8
|
+
- 2
|
9
|
+
- 2
|
10
|
+
version: 2.2.2
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Tobias Luetke
|
@@ -17,7 +17,7 @@ autorequire:
|
|
17
17
|
bindir: bin
|
18
18
|
cert_chain: []
|
19
19
|
|
20
|
-
date: 2010-
|
20
|
+
date: 2010-10-01 00:00:00 -07:00
|
21
21
|
default_executable:
|
22
22
|
dependencies: []
|
23
23
|
|
@@ -32,12 +32,12 @@ extensions: []
|
|
32
32
|
|
33
33
|
extra_rdoc_files:
|
34
34
|
- History.txt
|
35
|
-
- README.
|
35
|
+
- README.md
|
36
36
|
files:
|
37
37
|
- CHANGELOG
|
38
38
|
- History.txt
|
39
39
|
- MIT-LICENSE
|
40
|
-
- README.
|
40
|
+
- README.md
|
41
41
|
- Rakefile
|
42
42
|
- init.rb
|
43
43
|
- lib/extras/liquid_view.rb
|
@@ -65,6 +65,7 @@ files:
|
|
65
65
|
- lib/liquid/tags/ifchanged.rb
|
66
66
|
- lib/liquid/tags/include.rb
|
67
67
|
- lib/liquid/tags/inherited_block.rb
|
68
|
+
- lib/liquid/tags/literal.rb
|
68
69
|
- lib/liquid/tags/unless.rb
|
69
70
|
- lib/liquid/template.rb
|
70
71
|
- lib/liquid/variable.rb
|
@@ -77,7 +78,7 @@ licenses: []
|
|
77
78
|
post_install_message:
|
78
79
|
rdoc_options:
|
79
80
|
- --main
|
80
|
-
- README.
|
81
|
+
- README.md
|
81
82
|
require_paths:
|
82
83
|
- lib
|
83
84
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -105,7 +106,7 @@ requirements: []
|
|
105
106
|
rubyforge_project: locomotive_liquid
|
106
107
|
rubygems_version: 1.3.7
|
107
108
|
signing_key:
|
108
|
-
specification_version:
|
109
|
+
specification_version: 3
|
109
110
|
summary: A secure, non-evaling end user template engine with aesthetic markup.
|
110
111
|
test_files: []
|
111
112
|
|
data/README.txt
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
= Liquid template engine
|
2
|
-
|
3
|
-
Liquid is a template engine which I wrote for very specific requirements
|
4
|
-
|
5
|
-
* It has to have beautiful and simple markup.
|
6
|
-
Template engines which don't produce good looking markup are no fun to use.
|
7
|
-
* 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.
|
8
|
-
* 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
|
9
|
-
just render it passing in a hash with local variables and objects.
|
10
|
-
|
11
|
-
== Why should i use Liquid
|
12
|
-
|
13
|
-
* 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.
|
14
|
-
* You want to render templates directly from the database
|
15
|
-
* You like smarty style template engines
|
16
|
-
* You need a template engine which does HTML just as well as Emails
|
17
|
-
* You don't like the markup of your current one
|
18
|
-
|
19
|
-
== What does it look like?
|
20
|
-
|
21
|
-
<ul id="products">
|
22
|
-
{% for product in products %}
|
23
|
-
<li>
|
24
|
-
<h2>{{product.name}}</h2>
|
25
|
-
Only {{product.price | price }}
|
26
|
-
|
27
|
-
{{product.description | prettyprint | paragraph }}
|
28
|
-
</li>
|
29
|
-
{% endfor %}
|
30
|
-
</ul>
|
31
|
-
|
32
|
-
== Howto use Liquid
|
33
|
-
|
34
|
-
Liquid supports a very simple API based around the Liquid::Template class.
|
35
|
-
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
36
|
-
|
37
|
-
@template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template
|
38
|
-
@template.render( 'name' => 'tobi' ) # => "hi tobi"
|