wordify_liquid 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. data/History.md +75 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +44 -0
  4. data/lib/extras/liquid_view.rb +51 -0
  5. data/lib/liquid.rb +68 -0
  6. data/lib/liquid/block.rb +115 -0
  7. data/lib/liquid/condition.rb +120 -0
  8. data/lib/liquid/context.rb +259 -0
  9. data/lib/liquid/document.rb +17 -0
  10. data/lib/liquid/drop.rb +61 -0
  11. data/lib/liquid/errors.rb +11 -0
  12. data/lib/liquid/extensions.rb +62 -0
  13. data/lib/liquid/file_system.rb +62 -0
  14. data/lib/liquid/htmltags.rb +74 -0
  15. data/lib/liquid/interrupts.rb +17 -0
  16. data/lib/liquid/module_ex.rb +62 -0
  17. data/lib/liquid/standardfilters.rb +245 -0
  18. data/lib/liquid/strainer.rb +53 -0
  19. data/lib/liquid/tag.rb +26 -0
  20. data/lib/liquid/tags/assign.rb +33 -0
  21. data/lib/liquid/tags/break.rb +21 -0
  22. data/lib/liquid/tags/capture.rb +35 -0
  23. data/lib/liquid/tags/case.rb +79 -0
  24. data/lib/liquid/tags/comment.rb +9 -0
  25. data/lib/liquid/tags/continue.rb +21 -0
  26. data/lib/liquid/tags/cycle.rb +59 -0
  27. data/lib/liquid/tags/decrement.rb +39 -0
  28. data/lib/liquid/tags/for.rb +142 -0
  29. data/lib/liquid/tags/if.rb +79 -0
  30. data/lib/liquid/tags/ifchanged.rb +20 -0
  31. data/lib/liquid/tags/include.rb +65 -0
  32. data/lib/liquid/tags/increment.rb +35 -0
  33. data/lib/liquid/tags/raw.rb +21 -0
  34. data/lib/liquid/tags/unless.rb +33 -0
  35. data/lib/liquid/template.rb +150 -0
  36. data/lib/liquid/utils.rb +31 -0
  37. data/lib/liquid/variable.rb +57 -0
  38. data/lib/wordify_liquid.rb +1 -0
  39. data/test/liquid/assign_test.rb +21 -0
  40. data/test/liquid/block_test.rb +58 -0
  41. data/test/liquid/capture_test.rb +40 -0
  42. data/test/liquid/condition_test.rb +127 -0
  43. data/test/liquid/context_test.rb +478 -0
  44. data/test/liquid/drop_test.rb +169 -0
  45. data/test/liquid/error_handling_test.rb +81 -0
  46. data/test/liquid/file_system_test.rb +29 -0
  47. data/test/liquid/filter_test.rb +125 -0
  48. data/test/liquid/module_ex_test.rb +87 -0
  49. data/test/liquid/output_test.rb +116 -0
  50. data/test/liquid/parsing_quirks_test.rb +52 -0
  51. data/test/liquid/regexp_test.rb +44 -0
  52. data/test/liquid/security_test.rb +64 -0
  53. data/test/liquid/standard_filter_test.rb +195 -0
  54. data/test/liquid/strainer_test.rb +52 -0
  55. data/test/liquid/tags/break_tag_test.rb +16 -0
  56. data/test/liquid/tags/continue_tag_test.rb +16 -0
  57. data/test/liquid/tags/for_tag_test.rb +284 -0
  58. data/test/liquid/tags/html_tag_test.rb +63 -0
  59. data/test/liquid/tags/if_else_tag_test.rb +160 -0
  60. data/test/liquid/tags/include_tag_test.rb +139 -0
  61. data/test/liquid/tags/increment_tag_test.rb +24 -0
  62. data/test/liquid/tags/raw_tag_test.rb +15 -0
  63. data/test/liquid/tags/standard_tag_test.rb +295 -0
  64. data/test/liquid/tags/statements_test.rb +134 -0
  65. data/test/liquid/tags/unless_else_tag_test.rb +26 -0
  66. data/test/liquid/template_test.rb +74 -0
  67. data/test/liquid/variable_test.rb +180 -0
  68. data/test/test_helper.rb +29 -0
  69. metadata +145 -0
@@ -0,0 +1,259 @@
1
+ module Liquid
2
+
3
+ # Context keeps the variable stack and resolves variables, as well as keywords
4
+ #
5
+ # context['variable'] = 'testing'
6
+ # context['variable'] #=> 'testing'
7
+ # context['true'] #=> true
8
+ # context['10.2232'] #=> 10.2232
9
+ #
10
+ # context.stack do
11
+ # context['bob'] = 'bobsen'
12
+ # end
13
+ #
14
+ # context['bob'] #=> nil class Context
15
+ class Context
16
+ attr_reader :scopes, :errors, :registers, :environments
17
+
18
+ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false)
19
+ @environments = [environments].flatten
20
+ @scopes = [(outer_scope || {})]
21
+ @registers = registers
22
+ @errors = []
23
+ @rethrow_errors = rethrow_errors
24
+ squash_instance_assigns_with_environments
25
+
26
+ @interrupts = []
27
+ end
28
+
29
+ def strainer
30
+ @strainer ||= Strainer.create(self)
31
+ end
32
+
33
+ # Adds filters to this context.
34
+ #
35
+ # Note that this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
36
+ # for that
37
+ def add_filters(filters)
38
+ filters = [filters].flatten.compact
39
+
40
+ filters.each do |f|
41
+ raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
42
+ Strainer.add_known_filter(f)
43
+ strainer.extend(f)
44
+ end
45
+ end
46
+
47
+ # are there any not handled interrupts?
48
+ def has_interrupt?
49
+ !@interrupts.empty?
50
+ end
51
+
52
+ # push an interrupt to the stack. this interrupt is considered not handled.
53
+ def push_interrupt(e)
54
+ @interrupts.push(e)
55
+ end
56
+
57
+ # pop an interrupt from the stack
58
+ def pop_interrupt
59
+ @interrupts.pop
60
+ end
61
+
62
+ def handle_error(e)
63
+ errors.push(e)
64
+ raise if @rethrow_errors
65
+
66
+ case e
67
+ when SyntaxError
68
+ "Liquid syntax error: #{e.message}"
69
+ else
70
+ "Liquid error: #{e.message}"
71
+ end
72
+ end
73
+
74
+ def invoke(method, *args)
75
+ strainer.invoke(method, *args)
76
+ end
77
+
78
+ # Push new local scope on the stack. use <tt>Context#stack</tt> instead
79
+ def push(new_scope={})
80
+ @scopes.unshift(new_scope)
81
+ raise StackLevelError, "Nesting too deep" if @scopes.length > 100
82
+ end
83
+
84
+ # Merge a hash of variables in the current local scope
85
+ def merge(new_scopes)
86
+ @scopes[0].merge!(new_scopes)
87
+ end
88
+
89
+ # Pop from the stack. use <tt>Context#stack</tt> instead
90
+ def pop
91
+ raise ContextError if @scopes.size == 1
92
+ @scopes.shift
93
+ end
94
+
95
+ # Pushes a new local scope on the stack, pops it at the end of the block
96
+ #
97
+ # Example:
98
+ # context.stack do
99
+ # context['var'] = 'hi'
100
+ # end
101
+ #
102
+ # context['var] #=> nil
103
+ def stack(new_scope={})
104
+ push(new_scope)
105
+ yield
106
+ ensure
107
+ pop
108
+ end
109
+
110
+ def clear_instance_assigns
111
+ @scopes[0] = {}
112
+ end
113
+
114
+ # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
115
+ def []=(key, value)
116
+ @scopes[0][key] = value
117
+ end
118
+
119
+ def [](key)
120
+ resolve(key)
121
+ end
122
+
123
+ def has_key?(key)
124
+ resolve(key) != nil
125
+ end
126
+
127
+ private
128
+ LITERALS = {
129
+ nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
130
+ 'true' => true,
131
+ 'false' => false,
132
+ 'blank' => :blank?,
133
+ 'empty' => :empty?
134
+ }
135
+
136
+ # Look up variable, either resolve directly after considering the name. We can directly handle
137
+ # Strings, digits, floats and booleans (true,false).
138
+ # If no match is made we lookup the variable in the current scope and
139
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
140
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
141
+ #
142
+ # Example:
143
+ # products == empty #=> products.empty?
144
+ def resolve(key)
145
+ if LITERALS.key?(key)
146
+ LITERALS[key]
147
+ else
148
+ case key
149
+ when /^'(.*)'$/ # Single quoted strings
150
+ $1
151
+ when /^"(.*)"$/ # Double quoted strings
152
+ $1
153
+ when /^(-?\d+)$/ # Integer and floats
154
+ $1.to_i
155
+ when /^\((\S+)\.\.(\S+)\)$/ # Ranges
156
+ (resolve($1).to_i..resolve($2).to_i)
157
+ when /^(-?\d[\d\.]+)$/ # Floats
158
+ $1.to_f
159
+ else
160
+ variable(key)
161
+ end
162
+ end
163
+ end
164
+
165
+ # Fetches an object starting at the local scope and then moving up the hierachy
166
+ def find_variable(key)
167
+ scope = @scopes.find { |s| s.has_key?(key) }
168
+
169
+ if scope.nil?
170
+ @environments.each do |e|
171
+ if variable = lookup_and_evaluate(e, key)
172
+ scope = e
173
+ break
174
+ end
175
+ end
176
+ end
177
+
178
+ scope ||= @environments.last || @scopes.last
179
+ variable ||= lookup_and_evaluate(scope, key)
180
+
181
+ variable = variable.to_liquid
182
+ variable.context = self if variable.respond_to?(:context=)
183
+
184
+ return variable
185
+ end
186
+
187
+ # Resolves namespaced queries gracefully.
188
+ #
189
+ # Example
190
+ # @context['hash'] = {"name" => 'tobi'}
191
+ # assert_equal 'tobi', @context['hash.name']
192
+ # assert_equal 'tobi', @context['hash["name"]']
193
+ def variable(markup)
194
+ parts = markup.scan(VariableParser)
195
+ square_bracketed = /^\[(.*)\]$/
196
+
197
+ first_part = parts.shift
198
+
199
+ if first_part =~ square_bracketed
200
+ first_part = resolve($1)
201
+ end
202
+
203
+ if object = find_variable(first_part)
204
+
205
+ parts.each do |part|
206
+ part = resolve($1) if part_resolved = (part =~ square_bracketed)
207
+
208
+ # If object is a hash- or array-like object we look for the
209
+ # presence of the key and if its available we return it
210
+ if object.respond_to?(:[]) and
211
+ ((object.respond_to?(:has_key?) and object.has_key?(part)) or
212
+ (object.respond_to?(:fetch) and part.is_a?(Integer)))
213
+
214
+ # if its a proc we will replace the entry with the proc
215
+ res = lookup_and_evaluate(object, part)
216
+ object = res.to_liquid
217
+
218
+ # Some special cases. If the part wasn't in square brackets and
219
+ # no key with the same name was found we interpret following calls
220
+ # as commands and call them on the current object
221
+ elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
222
+
223
+ object = object.send(part.intern).to_liquid
224
+
225
+ # No key was present with the desired value and it wasn't one of the directly supported
226
+ # keywords either. The only thing we got left is to return nil
227
+ else
228
+ return nil
229
+ end
230
+
231
+ # If we are dealing with a drop here we have to
232
+ object.context = self if object.respond_to?(:context=)
233
+ end
234
+ end
235
+
236
+ object
237
+ end # variable
238
+
239
+ def lookup_and_evaluate(obj, key)
240
+ if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
241
+ obj[key] = (value.arity == 0) ? value.call : value.call(self)
242
+ else
243
+ value
244
+ end
245
+ end # lookup_and_evaluate
246
+
247
+ def squash_instance_assigns_with_environments
248
+ @scopes.last.each_key do |k|
249
+ @environments.each do |env|
250
+ if env.has_key?(k)
251
+ scopes.last[k] = lookup_and_evaluate(env, k)
252
+ break
253
+ end
254
+ end
255
+ end
256
+ end # squash_instance_assigns_with_environments
257
+ end # Context
258
+
259
+ end # Liquid
@@ -0,0 +1,17 @@
1
+ module Liquid
2
+ class Document < Block
3
+ # we don't need markup to open this block
4
+ def initialize(tokens)
5
+ parse(tokens)
6
+ end
7
+
8
+ # There isn't a real delimter
9
+ def block_delimiter
10
+ []
11
+ end
12
+
13
+ # Document blocks don't need to be terminated since they are not actually opened
14
+ def assert_missing_delimitation!
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,61 @@
1
+ require 'set'
2
+
3
+ module Liquid
4
+
5
+ # A drop in liquid is a class which allows you to export DOM like things to liquid.
6
+ # Methods of drops are callable.
7
+ # The main use for liquid drops is to implement lazy loaded objects.
8
+ # If you would like to make data available to the web designers which you don't want loaded unless needed then
9
+ # a drop is a great way to do that.
10
+ #
11
+ # Example:
12
+ #
13
+ # class ProductDrop < Liquid::Drop
14
+ # def top_sales
15
+ # Shop.current.products.find(:all, :order => 'sales', :limit => 10 )
16
+ # end
17
+ # end
18
+ #
19
+ # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
20
+ # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
21
+ #
22
+ # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
23
+ # catch all.
24
+ class Drop
25
+ attr_writer :context
26
+
27
+ EMPTY_STRING = ''.freeze
28
+
29
+ # Catch all for the method
30
+ def before_method(method)
31
+ nil
32
+ end
33
+
34
+ # called by liquid to invoke a drop
35
+ def invoke_drop(method_or_key)
36
+ if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
37
+ send(method_or_key)
38
+ else
39
+ before_method(method_or_key)
40
+ end
41
+ end
42
+
43
+ def has_key?(name)
44
+ true
45
+ end
46
+
47
+ def to_liquid
48
+ self
49
+ end
50
+
51
+ alias :[] :invoke_drop
52
+
53
+ private
54
+
55
+ # Check for method existence without invoking respond_to?, which creates symbols
56
+ def self.invokable?(method_name)
57
+ @invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
58
+ @invokable_methods.include?(method_name.to_s)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,11 @@
1
+ module Liquid
2
+ class Error < ::StandardError; end
3
+
4
+ class ArgumentError < Error; end
5
+ class ContextError < Error; end
6
+ class FilterNotFound < Error; end
7
+ class FileSystemError < Error; end
8
+ class StandardError < Error; end
9
+ class SyntaxError < Error; end
10
+ class StackLevelError < Error; end
11
+ end
@@ -0,0 +1,62 @@
1
+ require 'time'
2
+ require 'date'
3
+
4
+ class String # :nodoc:
5
+ def to_liquid
6
+ self
7
+ end
8
+ end
9
+
10
+ class Array # :nodoc:
11
+ def to_liquid
12
+ self
13
+ end
14
+ end
15
+
16
+ class Hash # :nodoc:
17
+ def to_liquid
18
+ self
19
+ end
20
+ end
21
+
22
+ class Numeric # :nodoc:
23
+ def to_liquid
24
+ self
25
+ end
26
+ end
27
+
28
+ class Time # :nodoc:
29
+ def to_liquid
30
+ self
31
+ end
32
+ end
33
+
34
+ class DateTime < Date # :nodoc:
35
+ def to_liquid
36
+ self
37
+ end
38
+ end
39
+
40
+ class Date # :nodoc:
41
+ def to_liquid
42
+ self
43
+ end
44
+ end
45
+
46
+ class TrueClass
47
+ def to_liquid # :nodoc:
48
+ self
49
+ end
50
+ end
51
+
52
+ class FalseClass
53
+ def to_liquid # :nodoc:
54
+ self
55
+ end
56
+ end
57
+
58
+ class NilClass
59
+ def to_liquid # :nodoc:
60
+ self
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ module Liquid
2
+ # A Liquid file system is way to let your templates retrieve other templates for use with the include tag.
3
+ #
4
+ # You can implement subclasses that retrieve templates from the database, from the file system using a different
5
+ # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit.
6
+ #
7
+ # You can add additional instance variables, arguments, or methods as needed.
8
+ #
9
+ # Example:
10
+ #
11
+ # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
12
+ # liquid = Liquid::Template.parse(template)
13
+ #
14
+ # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
15
+ class BlankFileSystem
16
+ # Called by Liquid to retrieve a template file
17
+ def read_template_file(template_path, context)
18
+ raise FileSystemError, "This liquid context does not allow includes."
19
+ end
20
+ end
21
+
22
+ # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials,
23
+ # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added.
24
+ #
25
+ # For security reasons, template paths are only allowed to contain letters, numbers, and underscore.
26
+ #
27
+ # Example:
28
+ #
29
+ # file_system = Liquid::LocalFileSystem.new("/some/path")
30
+ #
31
+ # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
32
+ # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
33
+ #
34
+ class LocalFileSystem
35
+ attr_accessor :root
36
+
37
+ def initialize(root)
38
+ @root = root
39
+ end
40
+
41
+ def read_template_file(template_path, context)
42
+ full_path = full_path(template_path)
43
+ raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
44
+
45
+ File.read(full_path)
46
+ end
47
+
48
+ def full_path(template_path)
49
+ raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
50
+
51
+ full_path = if template_path.include?('/')
52
+ File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
53
+ else
54
+ File.join(root, "_#{template_path}.liquid")
55
+ end
56
+
57
+ raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
58
+
59
+ full_path
60
+ end
61
+ end
62
+ end