locomotivecms-solid 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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