locomotivecms-solid 0.2.2

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +20 -0
  7. data/README.md +152 -0
  8. data/Rakefile +7 -0
  9. data/lib/locomotivecms-solid.rb +2 -0
  10. data/lib/solid.rb +48 -0
  11. data/lib/solid/arguments.rb +26 -0
  12. data/lib/solid/block.rb +13 -0
  13. data/lib/solid/conditional_block.rb +35 -0
  14. data/lib/solid/context_error.rb +2 -0
  15. data/lib/solid/default_security_rules.rb +24 -0
  16. data/lib/solid/element.rb +51 -0
  17. data/lib/solid/engine.rb +4 -0
  18. data/lib/solid/extensions.rb +17 -0
  19. data/lib/solid/iterable.rb +18 -0
  20. data/lib/solid/liquid_extensions.rb +87 -0
  21. data/lib/solid/liquid_extensions/assign_tag.rb +21 -0
  22. data/lib/solid/liquid_extensions/for_tag.rb +102 -0
  23. data/lib/solid/liquid_extensions/if_tag.rb +44 -0
  24. data/lib/solid/liquid_extensions/unless_tag.rb +13 -0
  25. data/lib/solid/liquid_extensions/variable.rb +34 -0
  26. data/lib/solid/method_whitelist.rb +56 -0
  27. data/lib/solid/model_drop.rb +119 -0
  28. data/lib/solid/parser.rb +108 -0
  29. data/lib/solid/parser/ripper.rb +220 -0
  30. data/lib/solid/parser/ruby_parser.rb +88 -0
  31. data/lib/solid/tag.rb +11 -0
  32. data/lib/solid/template.rb +24 -0
  33. data/lib/solid/version.rb +3 -0
  34. data/locomotivecms-solid.gemspec +26 -0
  35. data/spec/solid/arguments_spec.rb +314 -0
  36. data/spec/solid/block_spec.rb +39 -0
  37. data/spec/solid/conditional_block_spec.rb +39 -0
  38. data/spec/solid/default_security_rules_spec.rb +180 -0
  39. data/spec/solid/element_examples.rb +67 -0
  40. data/spec/solid/liquid_extensions/assign_tag_spec.rb +27 -0
  41. data/spec/solid/liquid_extensions/for_tag_spec.rb +48 -0
  42. data/spec/solid/liquid_extensions/if_tag_spec.rb +64 -0
  43. data/spec/solid/liquid_extensions/unless_tag_spec.rb +54 -0
  44. data/spec/solid/liquid_extensions/variable_spec.rb +25 -0
  45. data/spec/solid/model_drop_spec.rb +26 -0
  46. data/spec/solid/parser/ripper_spec.rb +14 -0
  47. data/spec/solid/parser/ruby_parser_spec.rb +7 -0
  48. data/spec/solid/tag_spec.rb +26 -0
  49. data/spec/solid/template_spec.rb +37 -0
  50. data/spec/spec_helper.rb +8 -0
  51. data/spec/support/class_highjacker_examples.rb +33 -0
  52. data/spec/support/method_whitelist_matchers.rb +17 -0
  53. data/spec/support/parser_examples.rb +261 -0
  54. data/spec/support/tag_highjacker_examples.rb +33 -0
  55. metadata +204 -0
@@ -0,0 +1,18 @@
1
+ module Solid
2
+ module Iterable
3
+ include Enumerable
4
+
5
+ def each(&block)
6
+ self.walk(&block)
7
+ end
8
+
9
+ protected
10
+ def walk(nodes=nil, &block)
11
+ (nodes || self.nodelist).each do |node|
12
+ yield node
13
+ walk(node.nodelist || [], &block) if node.respond_to?(:nodelist)
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,87 @@
1
+ module Solid
2
+
3
+ class << self
4
+
5
+ def extend_liquid!
6
+ LiquidExtensions.load!
7
+ end
8
+
9
+ end
10
+
11
+ module LiquidExtensions
12
+
13
+ module ClassHighjacker
14
+
15
+ def load!
16
+ original_class = Liquid.send(:remove_const, demodulized_name)
17
+ original_classes[demodulized_name] = original_class unless original_classes.has_key?(demodulized_name) # avoid loosing reference to original class
18
+ Liquid.const_set(demodulized_name, self)
19
+ end
20
+
21
+ def unload!
22
+ if original_class = original_classes[demodulized_name]
23
+ Liquid.send(:remove_const, demodulized_name)
24
+ Liquid.const_set(demodulized_name, original_class)
25
+ end
26
+ end
27
+
28
+ def demodulized_name
29
+ @demodulized_name ||= self.name.split('::').last
30
+ end
31
+
32
+ protected
33
+ def original_classes
34
+ @@original_classes ||= {}
35
+ end
36
+
37
+ end
38
+
39
+ module TagHighjacker
40
+
41
+ def load!
42
+ original_tag = Liquid::Template.tags[tag_name.to_s]
43
+ original_tags[tag_name] = original_tag unless original_tags.has_key?(tag_name) # avoid loosing reference to original class
44
+ Liquid::Template.register_tag(tag_name, self)
45
+ end
46
+
47
+ def unload!
48
+ Liquid::Template.register_tag(tag_name, original_tags[tag_name])
49
+ end
50
+
51
+ def tag_name(name=nil)
52
+ @tag_name = name unless name.nil?
53
+ @tag_name
54
+ end
55
+
56
+ protected
57
+ def original_tags
58
+ @@original_tags ||= {}
59
+ end
60
+
61
+ end
62
+
63
+ BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'liquid_extensions')
64
+
65
+ # FIXME: Keep the original ForTag from being modified.
66
+ %w(if_tag unless_tag assign_tag variable).each do |mod|
67
+ require File.join(BASE_PATH, mod)
68
+ end
69
+
70
+ # FIXME: Keep the original ForTag from being modified.
71
+ ALL = [IfTag, UnlessTag, AssignTag, Variable]
72
+
73
+ class << self
74
+
75
+ def load!
76
+ ALL.each(&:load!)
77
+ end
78
+
79
+ def unload!
80
+ ALL.each(&:unload!)
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,21 @@
1
+ module Solid
2
+ module LiquidExtensions
3
+ class AssignTag < Solid::Tag
4
+ extend TagHighjacker
5
+
6
+ tag_name :assign
7
+
8
+ def initialize(tag_name, assignment, tokens, context = {})
9
+ @assigned_variable, expression = assignment.split('=', 2)
10
+ @assigned_variable = @assigned_variable.strip
11
+ super(tag_name, expression, tokens, context)
12
+ end
13
+
14
+ def display(expression_result)
15
+ current_context.scopes.last[@assigned_variable] = expression_result
16
+ ''
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,102 @@
1
+ module Solid
2
+ module LiquidExtensions
3
+
4
+ # This for block reimplementation is deliberately backward incompatible
5
+ # since all strange features supported by the original for loop like
6
+ # "reversed" or "limit: 20 offset: 40" are favourably replaced by pure ruby
7
+ # methods like `Array#reversed` or `Array#slice`
8
+ class ForTag < Solid::Block
9
+ extend TagHighjacker
10
+
11
+ tag_name :for
12
+
13
+ def initialize(tag_name, expression, tokens, context = {})
14
+ @variable_name, iterable_expression = expression.split(/\s+in\s+/, 2).map(&:strip)
15
+ super(tag_name, iterable_expression, tokens, context)
16
+ end
17
+
18
+ def display(collection)
19
+ forloop = loop_for(collection)
20
+ output = []
21
+ collection = [] unless collection.respond_to?(:each_with_index)
22
+ collection.each_with_index do |element, index|
23
+ current_context.stack do
24
+ current_context[@variable_name] = element.to_liquid
25
+ current_context['forloop'] = forloop
26
+ output << yield
27
+ forloop.inc!
28
+ end
29
+ end
30
+ output.join
31
+ end
32
+
33
+ protected
34
+ def loop_for(collection)
35
+ if paginated?(collection)
36
+ PaginatedForLoop.new(collection)
37
+ else
38
+ ForLoop.new(collection)
39
+ end
40
+ end
41
+
42
+ def paginated?(collection)
43
+ defined?(WillPaginate) and
44
+ (collection.singleton_class < WillPaginate::CollectionMethods or
45
+ collection.singleton_class < WillPaginate::Mongoid::CollectionMethods)
46
+ end
47
+
48
+ end
49
+
50
+ class ForLoop < Liquid::Drop
51
+
52
+ def initialize(collection)
53
+ @collection = collection
54
+ @index0 = 0
55
+ end
56
+
57
+ def index0
58
+ @index0
59
+ end
60
+
61
+ def index
62
+ index0 + 1
63
+ end
64
+
65
+ def rindex
66
+ length - index0
67
+ end
68
+
69
+ def rindex0
70
+ length - index0 - 1
71
+ end
72
+
73
+ def length
74
+ @collection.length
75
+ end
76
+
77
+ def first
78
+ index0 == 0
79
+ end
80
+
81
+ def last
82
+ index == length
83
+ end
84
+
85
+ def inc!
86
+ @index0 += 1
87
+ end
88
+
89
+ end
90
+
91
+ class PaginatedForLoop < ForLoop
92
+
93
+ def initialize(collection)
94
+ super
95
+ @index0 = collection.offset || 0
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+ end
102
+
@@ -0,0 +1,44 @@
1
+ module Solid
2
+ module LiquidExtensions
3
+ class IfTag < Liquid::Block
4
+ include Solid::Element
5
+ extend TagHighjacker
6
+
7
+ tag_name :if
8
+
9
+ def initialize(tag_name, expression, tokens, context = {})
10
+ @blocks = []
11
+ push_block!(expression)
12
+ super
13
+ end
14
+
15
+ def render(context)
16
+ with_context(context) do
17
+ @blocks.each do |expression, blocks|
18
+ if expression.evaluate(context)
19
+ return render_all(blocks, context)
20
+ end
21
+ end
22
+ end
23
+ ''
24
+ end
25
+
26
+ def unknown_tag(tag, expression, tokens, context = {})
27
+ if tag == 'elsif'
28
+ push_block!(expression)
29
+ elsif tag == 'else'
30
+ push_block!('true')
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ def push_block!(expression)
37
+ block = []
38
+ @blocks.push([Solid::Parser.parse(expression), block])
39
+ @nodelist = block
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ module Solid
2
+ module LiquidExtensions
3
+ class UnlessTag < Solid::LiquidExtensions::IfTag
4
+
5
+ tag_name :unless
6
+
7
+ def initialize(tag_name, expression, tokens, context = {})
8
+ super(tag_name, "!(#{expression})", tokens, context)
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ module Solid
2
+ module LiquidExtensions
3
+ class Variable < ::Liquid::Variable
4
+ extend ClassHighjacker
5
+
6
+ def initialize(markup, options={})
7
+ super
8
+ @expression = Solid::Parser.parse(@name)
9
+ end
10
+
11
+ def render(context)
12
+ return '' if @name.nil?
13
+ value = @expression.evaluate(context)
14
+ apply_filters_on(value, context)
15
+ end
16
+
17
+ protected
18
+
19
+ def apply_filters_on(value, context)
20
+ @filters.inject(value) do |output, filter|
21
+ filterargs = filter[1].to_a.collect do |a|
22
+ context[a]
23
+ end
24
+ begin
25
+ output = context.invoke(filter[0], output, *filterargs)
26
+ rescue FilterNotFound
27
+ raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,56 @@
1
+ module Solid
2
+ module MethodWhitelist
3
+ extend self
4
+
5
+ METHODS_WHITELIST = {}
6
+ METHODS_BLACKLIST = {}
7
+
8
+ class << self
9
+
10
+ def allow(rules)
11
+ rules.each do |owner, method_names|
12
+ list = METHODS_WHITELIST[owner] ||= Set.new
13
+ [method_names].flatten.each do |method_name|
14
+ list.add(method_name.to_sym)
15
+ end
16
+ end
17
+ self
18
+ end
19
+
20
+ def deny(rules)
21
+ rules.each do |owner, method_names|
22
+ list = METHODS_BLACKLIST[owner] ||= Set.new
23
+ [method_names].flatten.each do |method_name|
24
+ list.add(method_name.to_sym)
25
+ end
26
+ end
27
+ self
28
+ end
29
+
30
+ end
31
+
32
+ def safely_respond_to?(object, method)
33
+ return false unless object.respond_to?(method, false)
34
+ method = object.method(method)
35
+ (!inherited?(object, method) || whitelisted?(method)) && !blacklisted?(method)
36
+ end
37
+ module_function :safely_respond_to?
38
+
39
+ private
40
+
41
+ def whitelisted?(method)
42
+ METHODS_WHITELIST.has_key?(method.owner) && METHODS_WHITELIST[method.owner].include?(method.name)
43
+ end
44
+
45
+ def blacklisted?(method)
46
+ METHODS_BLACKLIST.has_key?(method.owner) && METHODS_BLACKLIST[method.owner].include?(method.name)
47
+ end
48
+
49
+ def inherited?(object, method)
50
+ method.owner != object.class && !object.methods(false).include?(method.name)
51
+ end
52
+
53
+ end
54
+ end
55
+
56
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'default_security_rules')
@@ -0,0 +1,119 @@
1
+ class Solid::ModelDrop < Liquid::Drop
2
+
3
+ module ModelExtension
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ def to_drop
9
+ "#{self.name}Drop".constantize.new(current_scope || self)
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+
16
+ class_attribute :dynamic_methods
17
+
18
+ class << self
19
+
20
+ def model(model_name=nil)
21
+ if model_name
22
+ @model_name = model_name
23
+ else
24
+ @model_name ||= self.name.gsub(/Drop$/, '')
25
+ end
26
+ end
27
+
28
+ def model_class
29
+ @model_class ||= self.model.to_s.camelize.constantize
30
+ end
31
+
32
+ def immutable_method(method_name)
33
+ self.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
34
+ def #{method_name}_with_immutation(*args, &block)
35
+ self.dup.tap do |clone|
36
+ clone.#{method_name}_without_immutation(*args, &block)
37
+ end
38
+ end
39
+ END_EVAL
40
+ self.alias_method_chain method_name, :immutation
41
+ end
42
+
43
+ def respond(options={})
44
+ raise ArgumentError.new(":to option should be a Regexp") unless options[:to].is_a?(Regexp)
45
+ raise ArgumentError.new(":with option is mandatory") unless options[:with].present?
46
+ self.dynamic_methods ||= []
47
+ self.dynamic_methods += [[options[:to], options[:with]]]
48
+ end
49
+
50
+ def allow_scopes(*scopes)
51
+ @allowed_scopes = scopes
52
+ scopes.each do |scope_name|
53
+ self.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
54
+ def #{scope_name}(*args)
55
+ @scope = scope.public_send(:#{scope_name}, *args)
56
+ end
57
+ END_EVAL
58
+ self.immutable_method(scope_name)
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ delegate :model_class, :to => 'self.class'
65
+
66
+ respond :to => /limited_to_(\d+)/, :with => :limit_to
67
+
68
+ def initialize(base_scope=nil, context=nil)
69
+ @scope = base_scope
70
+ @context ||= context
71
+ end
72
+
73
+ def all
74
+ self
75
+ end
76
+
77
+ def each(&block)
78
+ scope.each(&block)
79
+ end
80
+
81
+ def before_method(method_name, *args)
82
+ self.class.dynamic_methods.each do |pattern, method|
83
+ if match_data = pattern.match(method_name)
84
+ return self.send(method, *match_data[1..-1])
85
+ end
86
+ end
87
+ raise NoMethodError.new("undefined method `#{method_name}' for #{self.inspect}")
88
+ end
89
+
90
+ delegate :to_a, to: :each
91
+ delegate *(Array.public_instance_methods - self.public_instance_methods), to: :to_a
92
+
93
+ protected
94
+
95
+ def limit_to(size)
96
+ @scope = scope.limit(size.to_i)
97
+ end
98
+ immutable_method :limit_to
99
+
100
+ def scope
101
+ @scope ||= default_scope
102
+ end
103
+
104
+ def default_scope
105
+ model_class
106
+ end
107
+
108
+ def context
109
+ @context
110
+ end
111
+
112
+ private
113
+
114
+ if Rails.env.test? # Just for cleaner and simpler specs
115
+ def method_missing(name, *args, &block)
116
+ before_method(name.to_s)
117
+ end
118
+ end
119
+ end