locomotivecms_solid 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +9 -0
  5. data/Gemfile +4 -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/arguments.rb +28 -0
  11. data/lib/solid/block.rb +13 -0
  12. data/lib/solid/conditional_block.rb +35 -0
  13. data/lib/solid/context_error.rb +2 -0
  14. data/lib/solid/default_security_rules.rb +24 -0
  15. data/lib/solid/element.rb +51 -0
  16. data/lib/solid/engine.rb +4 -0
  17. data/lib/solid/extensions.rb +17 -0
  18. data/lib/solid/iterable.rb +18 -0
  19. data/lib/solid/liquid_extensions/assign_tag.rb +21 -0
  20. data/lib/solid/liquid_extensions/for_tag.rb +102 -0
  21. data/lib/solid/liquid_extensions/if_tag.rb +44 -0
  22. data/lib/solid/liquid_extensions/unless_tag.rb +13 -0
  23. data/lib/solid/liquid_extensions/variable.rb +34 -0
  24. data/lib/solid/liquid_extensions.rb +87 -0
  25. data/lib/solid/method_whitelist.rb +56 -0
  26. data/lib/solid/model_drop.rb +119 -0
  27. data/lib/solid/parser.rb +265 -0
  28. data/lib/solid/tag.rb +11 -0
  29. data/lib/solid/template.rb +24 -0
  30. data/lib/solid/version.rb +3 -0
  31. data/lib/solid.rb +48 -0
  32. data/locomotivecms_solid.gemspec +25 -0
  33. data/spec/solid/arguments_spec.rb +310 -0
  34. data/spec/solid/block_spec.rb +39 -0
  35. data/spec/solid/conditional_block_spec.rb +39 -0
  36. data/spec/solid/default_security_rules_spec.rb +180 -0
  37. data/spec/solid/element_examples.rb +67 -0
  38. data/spec/solid/liquid_extensions/assign_tag_spec.rb +27 -0
  39. data/spec/solid/liquid_extensions/for_tag_spec.rb +48 -0
  40. data/spec/solid/liquid_extensions/if_tag_spec.rb +64 -0
  41. data/spec/solid/liquid_extensions/unless_tag_spec.rb +54 -0
  42. data/spec/solid/liquid_extensions/variable_spec.rb +25 -0
  43. data/spec/solid/model_drop_spec.rb +26 -0
  44. data/spec/solid/tag_spec.rb +26 -0
  45. data/spec/solid/template_spec.rb +37 -0
  46. data/spec/spec_helper.rb +8 -0
  47. data/spec/support/class_highjacker_examples.rb +33 -0
  48. data/spec/support/method_whitelist_matchers.rb +17 -0
  49. data/spec/support/tag_highjacker_examples.rb +33 -0
  50. metadata +201 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.2@solid
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - ruby-head
6
+ - rbx-19mode
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in solid.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Tigerlily, http://tigerlilyapps.com/
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Solid
2
+
3
+ Solid aim to provide a easier and nicer API to create custom Liquid tags and blocks
4
+
5
+ ## Installation
6
+
7
+ Due to a name conflict the gem is called tigerlily-solid. So to use it:
8
+
9
+ ```
10
+ gem 'tigerlily-solid', :require => 'solid'
11
+ ```
12
+
13
+ ## Build Status [![Build Status](https://secure.travis-ci.org/tigerlily/solid.png)](http://travis-ci.org/tigerlily/solid)
14
+
15
+ ## Tags
16
+
17
+ To create a new tag, you just have to:
18
+
19
+ - Extend `Solid::Tag`
20
+ - Define a `display` method
21
+ - Give a `tag_name`
22
+
23
+ ```ruby
24
+ class DummyTag < Solid::Tag
25
+
26
+ tag_name :dummy # register in Liquid under the name of `dummy`
27
+
28
+ def display
29
+ 'dummy !!!'
30
+ end
31
+
32
+ end
33
+ ```
34
+
35
+ ```html
36
+ <p>{% dummy %}<p>
37
+ ```
38
+
39
+ ## Arguments
40
+
41
+ This is the simpliest tag ever but, Solid tags can receive rich arguments:
42
+
43
+ ```ruby
44
+ class TypeOfTag < Solid::Tag
45
+
46
+ tag_name :typeof
47
+
48
+ def display(*values)
49
+ ''.tap do |output|
50
+ values.each do |value|
51
+ output << "<p>Type of #{value} is #{value.class.name}</p>"
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ ```
58
+
59
+ ```html
60
+ {% capture myvar %}eggspam{% endcapture %}
61
+ {% typeof "foo", 42, 4.2, myvar, myoption:"bar", otheroption:myvar %}
62
+ <!-- produce -->
63
+ <p>Type of "foo" is String</p>
64
+ <p>Type of 42 is Integer</p>
65
+ <p>Type of 4.2 is Float</p>
66
+ <p>Type of "eggspam" is String</p>
67
+ <p>Type of {:myoption=>"bar", :otheroption=>"eggspam"} is Hash</p>
68
+ ```
69
+
70
+ ## Context attributes
71
+
72
+ If there is some "global variables" in your liquid context you can declare that
73
+ your tag need to access it:
74
+
75
+ ```ruby
76
+ class HelloTag < Solid::Tag
77
+
78
+ tag_name :hello
79
+
80
+ context_attribute :current_user
81
+
82
+ def display
83
+ "Hello #{current_user.name} !"
84
+ end
85
+
86
+ end
87
+ ```
88
+
89
+ ```html
90
+ <p>{% hello %}</p>
91
+ <!-- produce -->
92
+ <p>Hello Homer</p>
93
+ ```
94
+ ## Blocks
95
+
96
+ Block are just tags with a body. They perform the same argument parsing.
97
+ To render the block body from it's `display` method you just have to `yield`:
98
+
99
+ ```ruby
100
+ class PBlock < Solid::Block
101
+
102
+ tag_name :p
103
+
104
+ def display(options)
105
+ "<p class='#{options[:class]}'>#{yield}</p>"
106
+ end
107
+
108
+ end
109
+ ```
110
+
111
+ ```html
112
+ {% p class:"content" %}
113
+ It works !
114
+ {% endp %}
115
+ <!-- produce -->
116
+ <p class="content">It works !</p>
117
+ ```
118
+
119
+ Of course you are free to yield once, multiple times or even never.
120
+
121
+ ## Conditional Blocks
122
+
123
+ Conditional blocks are blocks with two bodies. If you yield `true` you will receive the main block
124
+ and if you yield `false` you will receive the else block:
125
+
126
+ ```ruby
127
+ class IfAuthorizedToTag < Solid::ConditionalBlock
128
+
129
+ tag_name :if_authorized_to
130
+
131
+ context_attribute :current_user
132
+
133
+ def display(permission)
134
+ yield(current_user.authorized_to?(permission))
135
+ end
136
+
137
+ end
138
+ ```
139
+
140
+ ```html
141
+ {% if_authorized_to "publish" %}
142
+ You are authorized !
143
+ {% else %}
144
+ Get out !
145
+ {% endif_authorized_to %}
146
+ ```
147
+
148
+ ## License
149
+
150
+ Solid is released under the MIT license:
151
+
152
+ http://www.opensource.org/licenses/MIT
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
@@ -0,0 +1,2 @@
1
+ # Thanks to this file, the gem will be automatically loaded at startup.
2
+ require 'solid'
@@ -0,0 +1,28 @@
1
+ require 'ripper'
2
+
3
+ class Solid::Arguments
4
+ include Enumerable
5
+
6
+ def self.parse(string)
7
+ new("[#{string}]").parse!
8
+ end
9
+
10
+ attr_accessor :values
11
+
12
+ def initialize(string)
13
+ @string = string
14
+ end
15
+
16
+ def parse!
17
+ self.values = Solid::Parser.parse(@string)
18
+ self
19
+ end
20
+
21
+ def each(*args, &block)
22
+ values.each(*args, &block)
23
+ end
24
+
25
+ def interpolate(context)
26
+ values.evaluate(context)
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ class Solid::Block < Liquid::Block
2
+
3
+ include Solid::Element
4
+
5
+ def render(context)
6
+ with_context(context) do
7
+ display(*arguments.interpolate(context)) do
8
+ super
9
+ end
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,35 @@
1
+ class Solid::ConditionalBlock < Liquid::Block
2
+ include Solid::Element
3
+
4
+ def initialize(tag_name, variable, tokens, context = {})
5
+ @blocks = []
6
+ push_block!
7
+ super
8
+ end
9
+
10
+ def render(context)
11
+ with_context(context) do
12
+ display(*arguments.interpolate(context)) do |condition_satisfied|
13
+ block = condition_satisfied ? @blocks.first : @blocks[1]
14
+ render_all(block, context) if block
15
+ end
16
+ end
17
+ end
18
+
19
+ def unknown_tag(tag, markup, tokens, context = {})
20
+ if tag == 'else'
21
+ push_block!
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def push_block!
30
+ block = []
31
+ @blocks.push(block)
32
+ @nodelist = block
33
+ end
34
+
35
+ end
@@ -0,0 +1,2 @@
1
+ class Solid::ContextError < Exception
2
+ end
@@ -0,0 +1,24 @@
1
+ Solid::MethodWhitelist
2
+ .allow(
3
+ BasicObject => [:!, :!=, :==],
4
+ Object => [:present?, :blank?, :in?, :to_json],
5
+ Kernel => [:nil?, :!~],
6
+ Module => [:==],
7
+ Enumerable => [:sort],
8
+ Comparable => [:<, :<=, :==, :>, :>=, :between?],
9
+ Numeric => [:blank?,
10
+ :second, :seconds, :minute, :minutes, :hour, :hours, :day, :days, :week, :weeks,
11
+ :bytes, :kilobytes, :megabytes, :gigabytes, :terabytes, :petabytes, :exabytes],
12
+ Integer => [:multiple_of?, :month, :months, :year, :years, :to_json],
13
+ ).deny(
14
+ Module => [:const_get, :const_set, :const_defined?, :freeze, :ancestors],
15
+ Class => [:new, :allocate, :superclass],
16
+ )
17
+
18
+ if defined?(JSON::Ext::Generator::GeneratorMethods::Fixnum) &&
19
+ defined?(JSON::Ext::Generator::GeneratorMethods::Bignum)
20
+ Solid::MethodWhitelist.allow(
21
+ JSON::Ext::Generator::GeneratorMethods::Fixnum => [:to_json],
22
+ JSON::Ext::Generator::GeneratorMethods::Bignum => [:to_json],
23
+ )
24
+ end
@@ -0,0 +1,51 @@
1
+ module Solid::Element
2
+
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ base.send(:include, Solid::Iterable)
6
+ end
7
+
8
+ def initialize(tag_name, arguments_string, tokens, context = {})
9
+ super
10
+ @arguments = Solid::Arguments.parse(arguments_string)
11
+ end
12
+
13
+ def arguments
14
+ @arguments
15
+ end
16
+
17
+ def with_context(context)
18
+ previous_context = @current_context
19
+ @current_context = context
20
+ yield
21
+ ensure
22
+ @current_context = previous_context
23
+ end
24
+
25
+ def current_context
26
+ @current_context or raise Solid::ContextError.new("There is currently no context, do you forget to call render ?")
27
+ end
28
+
29
+ def display(*args)
30
+ raise NotImplementedError.new("Solid::Element implementations SHOULD define a #display method")
31
+ end
32
+
33
+ module ClassMethods
34
+
35
+ def tag_name(value=nil)
36
+ if value
37
+ @tag_name = value
38
+ Liquid::Template.register_tag(value.to_s, self)
39
+ end
40
+ @tag_name
41
+ end
42
+
43
+ def context_attribute(name)
44
+ define_method(name) do
45
+ Solid.unproxify(current_context[name.to_s])
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,4 @@
1
+ module Solid
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,17 @@
1
+ module LiquidSafe
2
+ def to_liquid
3
+ self
4
+ end
5
+ end
6
+
7
+ class Symbol
8
+ include LiquidSafe
9
+ end
10
+
11
+ class Regexp
12
+ include LiquidSafe
13
+ end
14
+
15
+ class Time
16
+ extend LiquidSafe
17
+ end
@@ -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,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)
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