curlybars 0.9.13
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.
- checksums.yaml +7 -0
- data/lib/curlybars.rb +108 -0
- data/lib/curlybars/configuration.rb +41 -0
- data/lib/curlybars/dependency_tracker.rb +8 -0
- data/lib/curlybars/error/base.rb +18 -0
- data/lib/curlybars/error/compile.rb +11 -0
- data/lib/curlybars/error/lex.rb +22 -0
- data/lib/curlybars/error/parse.rb +41 -0
- data/lib/curlybars/error/presenter/not_found.rb +23 -0
- data/lib/curlybars/error/render.rb +11 -0
- data/lib/curlybars/error/validate.rb +18 -0
- data/lib/curlybars/lexer.rb +60 -0
- data/lib/curlybars/method_whitelist.rb +69 -0
- data/lib/curlybars/node/block_helper_else.rb +108 -0
- data/lib/curlybars/node/boolean.rb +24 -0
- data/lib/curlybars/node/each_else.rb +69 -0
- data/lib/curlybars/node/if_else.rb +33 -0
- data/lib/curlybars/node/item.rb +31 -0
- data/lib/curlybars/node/literal.rb +28 -0
- data/lib/curlybars/node/option.rb +25 -0
- data/lib/curlybars/node/output.rb +24 -0
- data/lib/curlybars/node/partial.rb +24 -0
- data/lib/curlybars/node/path.rb +137 -0
- data/lib/curlybars/node/root.rb +29 -0
- data/lib/curlybars/node/string.rb +24 -0
- data/lib/curlybars/node/template.rb +32 -0
- data/lib/curlybars/node/text.rb +24 -0
- data/lib/curlybars/node/unless_else.rb +33 -0
- data/lib/curlybars/node/variable.rb +34 -0
- data/lib/curlybars/node/with_else.rb +54 -0
- data/lib/curlybars/parser.rb +183 -0
- data/lib/curlybars/position.rb +7 -0
- data/lib/curlybars/presenter.rb +288 -0
- data/lib/curlybars/processor/tilde.rb +31 -0
- data/lib/curlybars/processor/token_factory.rb +9 -0
- data/lib/curlybars/railtie.rb +18 -0
- data/lib/curlybars/rendering_support.rb +222 -0
- data/lib/curlybars/safe_buffer.rb +11 -0
- data/lib/curlybars/template_handler.rb +93 -0
- data/lib/curlybars/version.rb +3 -0
- data/spec/acceptance/application_layout_spec.rb +60 -0
- data/spec/acceptance/collection_blocks_spec.rb +28 -0
- data/spec/acceptance/global_helper_spec.rb +25 -0
- data/spec/curlybars/configuration_spec.rb +57 -0
- data/spec/curlybars/error/base_spec.rb +41 -0
- data/spec/curlybars/error/compile_spec.rb +19 -0
- data/spec/curlybars/error/lex_spec.rb +25 -0
- data/spec/curlybars/error/parse_spec.rb +74 -0
- data/spec/curlybars/error/render_spec.rb +19 -0
- data/spec/curlybars/error/validate_spec.rb +19 -0
- data/spec/curlybars/lexer_spec.rb +466 -0
- data/spec/curlybars/method_whitelist_spec.rb +168 -0
- data/spec/curlybars/processor/tilde_spec.rb +60 -0
- data/spec/curlybars/rendering_support_spec.rb +426 -0
- data/spec/curlybars/safe_buffer_spec.rb +46 -0
- data/spec/curlybars/template_handler_spec.rb +222 -0
- data/spec/integration/cache_spec.rb +124 -0
- data/spec/integration/comment_spec.rb +60 -0
- data/spec/integration/exception_spec.rb +31 -0
- data/spec/integration/node/block_helper_else_spec.rb +422 -0
- data/spec/integration/node/each_else_spec.rb +204 -0
- data/spec/integration/node/each_spec.rb +291 -0
- data/spec/integration/node/escape_spec.rb +27 -0
- data/spec/integration/node/helper_spec.rb +176 -0
- data/spec/integration/node/if_else_spec.rb +129 -0
- data/spec/integration/node/if_spec.rb +143 -0
- data/spec/integration/node/output_spec.rb +68 -0
- data/spec/integration/node/partial_spec.rb +66 -0
- data/spec/integration/node/path_spec.rb +286 -0
- data/spec/integration/node/root_spec.rb +15 -0
- data/spec/integration/node/template_spec.rb +86 -0
- data/spec/integration/node/unless_else_spec.rb +129 -0
- data/spec/integration/node/unless_spec.rb +130 -0
- data/spec/integration/node/with_spec.rb +116 -0
- data/spec/integration/processor/tilde_spec.rb +38 -0
- data/spec/integration/processors_spec.rb +30 -0
- metadata +358 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module Curlybars
|
2
|
+
module Node
|
3
|
+
Root = Struct.new(:template, :position) do
|
4
|
+
def compile
|
5
|
+
# NOTE: the following is a heredoc string, representing the ruby code fragment
|
6
|
+
# outputted by this node.
|
7
|
+
<<-RUBY
|
8
|
+
contexts = [presenter]
|
9
|
+
variables = [{}]
|
10
|
+
rendering = ::Curlybars::RenderingSupport.new(
|
11
|
+
::Curlybars.configuration.rendering_timeout,
|
12
|
+
contexts,
|
13
|
+
variables,
|
14
|
+
#{position.file_name.inspect},
|
15
|
+
global_helpers_providers,
|
16
|
+
::Curlybars.configuration.cache
|
17
|
+
)
|
18
|
+
buffer = ::Curlybars::SafeBuffer.new
|
19
|
+
#{template.compile}
|
20
|
+
buffer
|
21
|
+
RUBY
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate(branches)
|
25
|
+
template.validate(branches)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Curlybars
|
2
|
+
module Node
|
3
|
+
String = Struct.new(:string) do
|
4
|
+
def compile
|
5
|
+
# NOTE: the following is a heredoc string, representing the ruby code fragment
|
6
|
+
# outputted by this node.
|
7
|
+
<<-RUBY
|
8
|
+
->() { #{string.inspect} }
|
9
|
+
RUBY
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate(branches)
|
13
|
+
# Nothing to validate here.
|
14
|
+
end
|
15
|
+
|
16
|
+
def cache_key
|
17
|
+
[
|
18
|
+
string,
|
19
|
+
self.class.name
|
20
|
+
].join("/")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Curlybars
|
2
|
+
module Node
|
3
|
+
Template = Struct.new(:items, :position) do
|
4
|
+
def compile
|
5
|
+
compiled_items = items.map(&:compile).join("\n")
|
6
|
+
|
7
|
+
# NOTE: the following is a heredoc string, representing the ruby code fragment
|
8
|
+
# outputted by this node.
|
9
|
+
<<-RUBY
|
10
|
+
::Module.new do
|
11
|
+
def self.exec(contexts, rendering, variables, buffer)
|
12
|
+
unless contexts.length < ::Curlybars.configuration.nesting_limit
|
13
|
+
message = "Nesting too deep"
|
14
|
+
position = rendering.position(#{position.line_number}, #{position.line_offset})
|
15
|
+
raise ::Curlybars::Error::Render.new('nesting_too_deep', message, position)
|
16
|
+
end
|
17
|
+
#{compiled_items}
|
18
|
+
end
|
19
|
+
end.exec(contexts, rendering, variables, buffer)
|
20
|
+
RUBY
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate(branches)
|
24
|
+
items.map { |item| item.validate(branches) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_key
|
28
|
+
Digest::MD5.hexdigest(items.map(&:cache_key).join("/"))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Curlybars
|
2
|
+
module Node
|
3
|
+
Text = Struct.new(:text) do
|
4
|
+
def compile
|
5
|
+
# NOTE: the following is a heredoc string, representing the ruby code fragment
|
6
|
+
# outputted by this node.
|
7
|
+
<<-RUBY
|
8
|
+
buffer.concat(#{text.inspect}.html_safe)
|
9
|
+
RUBY
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate(branches)
|
13
|
+
# Nothing to validate here.
|
14
|
+
end
|
15
|
+
|
16
|
+
def cache_key
|
17
|
+
[
|
18
|
+
text.to_s,
|
19
|
+
self.class.name
|
20
|
+
].join("/")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Curlybars
|
2
|
+
module Node
|
3
|
+
UnlessElse = Struct.new(:expression, :unless_template, :else_template) do
|
4
|
+
def compile
|
5
|
+
# NOTE: the following is a heredoc string, representing the ruby code fragment
|
6
|
+
# outputted by this node.
|
7
|
+
<<-RUBY
|
8
|
+
unless rendering.to_bool(rendering.cached_call(#{expression.compile}))
|
9
|
+
#{unless_template.compile}
|
10
|
+
else
|
11
|
+
#{else_template.compile}
|
12
|
+
end
|
13
|
+
RUBY
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate(branches)
|
17
|
+
[
|
18
|
+
expression.validate(branches),
|
19
|
+
unless_template.validate(branches),
|
20
|
+
else_template.validate(branches)
|
21
|
+
]
|
22
|
+
end
|
23
|
+
|
24
|
+
def cache_key
|
25
|
+
[
|
26
|
+
expression,
|
27
|
+
unless_template,
|
28
|
+
else_template
|
29
|
+
].map(&:cache_key).push(self.class.name).join("/")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Curlybars
|
2
|
+
module Node
|
3
|
+
Variable = Struct.new(:variable, :position) do
|
4
|
+
def compile
|
5
|
+
# NOTE: the following is a heredoc string, representing the ruby code fragment
|
6
|
+
# outputted by this node.
|
7
|
+
<<-RUBY
|
8
|
+
-> {
|
9
|
+
position = rendering.position(
|
10
|
+
#{position.line_number},
|
11
|
+
#{position.line_offset}
|
12
|
+
)
|
13
|
+
rendering.variable(#{variable.inspect}, position)
|
14
|
+
}
|
15
|
+
RUBY
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate(branches)
|
19
|
+
# Nothing to validate here.
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_as_value(branches)
|
23
|
+
# It is always a value.
|
24
|
+
end
|
25
|
+
|
26
|
+
def cache_key
|
27
|
+
[
|
28
|
+
variable,
|
29
|
+
self.class.name
|
30
|
+
].join("/")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Curlybars
|
2
|
+
module Node
|
3
|
+
WithElse = Struct.new(:path, :with_template, :else_template, :position) do
|
4
|
+
def compile
|
5
|
+
# NOTE: the following is a heredoc string, representing the ruby code fragment
|
6
|
+
# outputted by this node.
|
7
|
+
<<-RUBY
|
8
|
+
compiled_path = rendering.cached_call(#{path.compile})
|
9
|
+
|
10
|
+
if rendering.to_bool(compiled_path)
|
11
|
+
position = rendering.position(#{position.line_number}, #{position.line_offset})
|
12
|
+
rendering.check_context_is_presenter(compiled_path, #{path.path.inspect}, position)
|
13
|
+
|
14
|
+
contexts.push(compiled_path)
|
15
|
+
begin
|
16
|
+
#{with_template.compile}
|
17
|
+
ensure
|
18
|
+
contexts.pop
|
19
|
+
end
|
20
|
+
else
|
21
|
+
#{else_template.compile}
|
22
|
+
end
|
23
|
+
RUBY
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate(branches)
|
27
|
+
sub_tree = path.resolve_and_check!(branches, check_type: :presenter)
|
28
|
+
with_template_errors = begin
|
29
|
+
branches.push(sub_tree)
|
30
|
+
with_template.validate(branches)
|
31
|
+
ensure
|
32
|
+
branches.pop
|
33
|
+
end
|
34
|
+
|
35
|
+
else_template_errors = else_template.validate(branches)
|
36
|
+
|
37
|
+
[
|
38
|
+
with_template_errors,
|
39
|
+
else_template_errors
|
40
|
+
]
|
41
|
+
rescue Curlybars::Error::Validate => path_error
|
42
|
+
path_error
|
43
|
+
end
|
44
|
+
|
45
|
+
def cache_key
|
46
|
+
[
|
47
|
+
path,
|
48
|
+
with_template,
|
49
|
+
else_template
|
50
|
+
].map(&:cache_key).push(self.class.name).join("/")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'rltk/parser'
|
2
|
+
|
3
|
+
require 'curlybars/node/root'
|
4
|
+
require 'curlybars/node/template'
|
5
|
+
require 'curlybars/node/item'
|
6
|
+
require 'curlybars/node/text'
|
7
|
+
require 'curlybars/node/if_else'
|
8
|
+
require 'curlybars/node/unless_else'
|
9
|
+
require 'curlybars/node/each_else'
|
10
|
+
require 'curlybars/node/path'
|
11
|
+
require 'curlybars/node/literal'
|
12
|
+
require 'curlybars/node/variable'
|
13
|
+
require 'curlybars/node/with_else'
|
14
|
+
require 'curlybars/node/block_helper_else'
|
15
|
+
require 'curlybars/node/option'
|
16
|
+
require 'curlybars/node/partial'
|
17
|
+
require 'curlybars/node/output'
|
18
|
+
|
19
|
+
module Curlybars
|
20
|
+
class Parser < RLTK::Parser
|
21
|
+
start :root
|
22
|
+
|
23
|
+
production(:root, 'template?') { |template| Node::Root.new(template || VOID, pos(0)) }
|
24
|
+
production(:template, 'items') { |items| Node::Template.new(items || [], pos(0)) }
|
25
|
+
|
26
|
+
production(:items) do
|
27
|
+
clause('items item') { |items, item| items << Node::Item.new(item) }
|
28
|
+
clause('item') { |item| [Node::Item.new(item)] }
|
29
|
+
end
|
30
|
+
|
31
|
+
production(:item) do
|
32
|
+
clause('TEXT') { |text| Node::Text.new(text) }
|
33
|
+
|
34
|
+
clause('START HASH .path .expressions? .options? END
|
35
|
+
.template?
|
36
|
+
START SLASH .path END') do |helper, arguments, options, template, helperclose|
|
37
|
+
Node::BlockHelperElse.new(
|
38
|
+
helper,
|
39
|
+
arguments || [],
|
40
|
+
options || [],
|
41
|
+
template || VOID,
|
42
|
+
VOID,
|
43
|
+
helperclose,
|
44
|
+
pos(0)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
clause('START HASH .path .expressions? .options? END
|
49
|
+
.template?
|
50
|
+
START ELSE END
|
51
|
+
.template?
|
52
|
+
START SLASH .path END') do |helper, arguments, options, helper_template, else_template, helperclose|
|
53
|
+
Node::BlockHelperElse.new(
|
54
|
+
helper,
|
55
|
+
arguments || [],
|
56
|
+
options || [],
|
57
|
+
helper_template || VOID,
|
58
|
+
else_template || VOID,
|
59
|
+
helperclose,
|
60
|
+
pos(0)
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
clause('START .path .expressions? .options? END') do |helper, arguments, options|
|
65
|
+
Node::BlockHelperElse.new(
|
66
|
+
helper,
|
67
|
+
arguments || [],
|
68
|
+
options || [],
|
69
|
+
VOID,
|
70
|
+
VOID,
|
71
|
+
helper,
|
72
|
+
pos(0)
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
clause('START .value END') do |value|
|
77
|
+
Node::Output.new(value)
|
78
|
+
end
|
79
|
+
|
80
|
+
clause('START HASH IF .expression END
|
81
|
+
.template?
|
82
|
+
START SLASH IF END') do |expression, if_template|
|
83
|
+
Node::IfElse.new(expression, if_template || VOID, VOID)
|
84
|
+
end
|
85
|
+
|
86
|
+
clause('START HASH IF .expression END
|
87
|
+
.template?
|
88
|
+
START ELSE END
|
89
|
+
.template?
|
90
|
+
START SLASH IF END') do |expression, if_template, else_template|
|
91
|
+
Node::IfElse.new(expression, if_template || VOID, else_template || VOID)
|
92
|
+
end
|
93
|
+
|
94
|
+
clause('START HASH UNLESS .expression END
|
95
|
+
.template?
|
96
|
+
START SLASH UNLESS END') do |expression, unless_template|
|
97
|
+
Node::UnlessElse.new(expression, unless_template || VOID, VOID)
|
98
|
+
end
|
99
|
+
|
100
|
+
clause('START HASH UNLESS .expression END
|
101
|
+
.template?
|
102
|
+
START ELSE END
|
103
|
+
.template?
|
104
|
+
START SLASH UNLESS END') do |expression, unless_template, else_template|
|
105
|
+
Node::UnlessElse.new(expression, unless_template || VOID, else_template || VOID)
|
106
|
+
end
|
107
|
+
|
108
|
+
clause('START HASH EACH .path END
|
109
|
+
.template?
|
110
|
+
START SLASH EACH END') do |path, each_template|
|
111
|
+
Node::EachElse.new(path, each_template || VOID, VOID, pos(0))
|
112
|
+
end
|
113
|
+
|
114
|
+
clause('START HASH EACH .path END
|
115
|
+
.template?
|
116
|
+
START ELSE END
|
117
|
+
.template?
|
118
|
+
START SLASH EACH END') do |path, each_template, else_template|
|
119
|
+
Node::EachElse.new(path, each_template || VOID, else_template || VOID, pos(0))
|
120
|
+
end
|
121
|
+
|
122
|
+
clause('START HASH WITH .path END
|
123
|
+
.template?
|
124
|
+
START SLASH WITH END') do |path, with_template|
|
125
|
+
Node::WithElse.new(path, with_template || VOID, VOID, pos(0))
|
126
|
+
end
|
127
|
+
|
128
|
+
clause('START HASH WITH .path END
|
129
|
+
.template?
|
130
|
+
START ELSE END
|
131
|
+
.template?
|
132
|
+
START SLASH WITH END') do |path, with_template, else_template|
|
133
|
+
Node::WithElse.new(path, with_template || VOID, else_template || VOID, pos(0))
|
134
|
+
end
|
135
|
+
|
136
|
+
clause('START GT .path END') do |path|
|
137
|
+
Node::Partial.new(path)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
production(:options) do
|
142
|
+
clause('options option') { |options, option| options << option }
|
143
|
+
clause('option') { |option| [option] }
|
144
|
+
end
|
145
|
+
|
146
|
+
production(:option, '.KEY .expression') do |key, expression|
|
147
|
+
Node::Option.new(key, expression)
|
148
|
+
end
|
149
|
+
|
150
|
+
production(:expressions) do
|
151
|
+
clause('expressions expression') { |expressions, expression| expressions << expression }
|
152
|
+
clause('expression') { |expression| [expression] }
|
153
|
+
end
|
154
|
+
|
155
|
+
production(:expression) do
|
156
|
+
clause('value') { |value| value }
|
157
|
+
clause('path') { |path| path }
|
158
|
+
end
|
159
|
+
|
160
|
+
production(:value) do
|
161
|
+
clause('LITERAL') { |literal| Node::Literal.new(literal) }
|
162
|
+
clause('VARIABLE') { |variable| Node::Variable.new(variable, pos(0)) }
|
163
|
+
end
|
164
|
+
|
165
|
+
production(:path, 'PATH') { |path| Node::Path.new(path, pos(0)) }
|
166
|
+
|
167
|
+
finalize
|
168
|
+
|
169
|
+
VOID = Class.new do
|
170
|
+
def compile
|
171
|
+
# Nothing to compile.
|
172
|
+
end
|
173
|
+
|
174
|
+
def validate(branches)
|
175
|
+
[] # Nothing to validate.
|
176
|
+
end
|
177
|
+
|
178
|
+
def cache_key
|
179
|
+
# Empty cache key.
|
180
|
+
end
|
181
|
+
end.new
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
require 'curlybars/method_whitelist'
|
2
|
+
require 'curlybars/configuration'
|
3
|
+
|
4
|
+
module Curlybars
|
5
|
+
# A base class that can be subclassed by concrete presenters.
|
6
|
+
#
|
7
|
+
# A Curlybars presenter is responsible for delivering data to templates, in the
|
8
|
+
# form of simple strings. Each public instance method on the presenter class
|
9
|
+
# can be referenced in a template. When a template is evaluated with a
|
10
|
+
# presenter, the referenced methods will be called with no arguments, and
|
11
|
+
# the returned strings inserted in place of the components in the template.
|
12
|
+
#
|
13
|
+
# Note that strings that are not HTML safe will be escaped.
|
14
|
+
#
|
15
|
+
# A presenter is always instantiated with a context to which it delegates
|
16
|
+
# unknown messages, usually an instance of ActionView::Base provided by
|
17
|
+
# Rails. See Curlybars::TemplateHandler for a typical use.
|
18
|
+
#
|
19
|
+
# Examples
|
20
|
+
#
|
21
|
+
# class BlogPresenter < Curlybars::Presenter
|
22
|
+
# presents :post
|
23
|
+
# allow_methods :title, :body, :author
|
24
|
+
#
|
25
|
+
# def title
|
26
|
+
# @post.title
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# def body
|
30
|
+
# markdown(@post.body)
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def author
|
34
|
+
# @post.author.full_name
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# presenter = BlogPresenter.new(context, post: post)
|
39
|
+
# presenter.author #=> "Jackie Chan"
|
40
|
+
#
|
41
|
+
class Presenter
|
42
|
+
extend Curlybars::MethodWhitelist
|
43
|
+
|
44
|
+
# Initializes the presenter with the given context and options.
|
45
|
+
#
|
46
|
+
# context - An ActionView::Base context.
|
47
|
+
# options - A Hash of options given to the presenter.
|
48
|
+
def initialize(context, options = {})
|
49
|
+
@_context = context
|
50
|
+
options.stringify_keys!
|
51
|
+
|
52
|
+
self.class.presented_names.each do |name|
|
53
|
+
value = options.fetch(name) do
|
54
|
+
default_values.fetch(name) do
|
55
|
+
block = default_blocks.fetch(name) do
|
56
|
+
raise ArgumentError.new("required identifier `#{name}` missing")
|
57
|
+
end
|
58
|
+
|
59
|
+
instance_exec(name, &block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
instance_variable_set("@#{name}", value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sets up the view.
|
68
|
+
#
|
69
|
+
# Override this method in your presenter in order to do setup before the
|
70
|
+
# template is rendered. One use case is to call `content_for` in order
|
71
|
+
# to inject content into other templates, e.g. a layout.
|
72
|
+
#
|
73
|
+
# Examples
|
74
|
+
#
|
75
|
+
# class Posts::ShowPresenter < Curlybars::Presenter
|
76
|
+
# presents :post
|
77
|
+
#
|
78
|
+
# def setup!
|
79
|
+
# content_for :page_title, @post.title
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# Returns nothing.
|
84
|
+
def setup!
|
85
|
+
# Does nothing.
|
86
|
+
end
|
87
|
+
|
88
|
+
# The key that should be used to cache the view.
|
89
|
+
#
|
90
|
+
# Unless `#cache_key` returns nil, the result of rendering the template
|
91
|
+
# that the presenter supports will be cached. The return value will be
|
92
|
+
# part of the final cache key, along with a digest of the template itself.
|
93
|
+
#
|
94
|
+
# Any object can be used as a cache key, so long as it
|
95
|
+
#
|
96
|
+
# - is a String,
|
97
|
+
# - responds to #cache_key itself, or
|
98
|
+
# - is an Array or a Hash whose items themselves fit either of these
|
99
|
+
# criteria.
|
100
|
+
#
|
101
|
+
# Returns the cache key Object or nil if no caching should be performed.
|
102
|
+
def cache_key
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
# The options that should be passed to the cache backend when caching the
|
107
|
+
# view. The exact options may vary depending on the backend you're using.
|
108
|
+
#
|
109
|
+
# The most common option is `:expires_in`, which controls the duration of
|
110
|
+
# time that the cached view should be considered fresh. Because it's so
|
111
|
+
# common, you can set that option simply by defining `#cache_duration`.
|
112
|
+
#
|
113
|
+
# Note: if you set the `:expires_in` option through this method, the
|
114
|
+
# `#cache_duration` value will be ignored.
|
115
|
+
#
|
116
|
+
# Returns a Hash.
|
117
|
+
def cache_options
|
118
|
+
{}
|
119
|
+
end
|
120
|
+
|
121
|
+
# The duration that the view should be cached for. Only relevant if
|
122
|
+
# `#cache_key` returns a non nil value.
|
123
|
+
#
|
124
|
+
# If nil, the view will not have an expiration time set. See also
|
125
|
+
# `#cache_options` for a more flexible way to set cache options.
|
126
|
+
#
|
127
|
+
# Examples
|
128
|
+
#
|
129
|
+
# def cache_duration
|
130
|
+
# 10.minutes
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# Returns the Fixnum duration of the cache item, in seconds, or nil if no
|
134
|
+
# duration should be set.
|
135
|
+
def cache_duration
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
|
139
|
+
class << self
|
140
|
+
# The name of the presenter class for a given view path.
|
141
|
+
#
|
142
|
+
# path - The String path of a view.
|
143
|
+
#
|
144
|
+
# Examples
|
145
|
+
#
|
146
|
+
# Curlybars::TemplateHandler.presenter_name_for_path("foo/bar")
|
147
|
+
# #=> "Foo::BarPresenter"
|
148
|
+
#
|
149
|
+
# Returns the String name of the matching presenter class.
|
150
|
+
def presenter_name_for_path(path)
|
151
|
+
"#{path}_presenter".camelize
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns the presenter class for the given path.
|
155
|
+
#
|
156
|
+
# path - The String path of a template.
|
157
|
+
#
|
158
|
+
# Returns the Class or nil if the constant cannot be found.
|
159
|
+
def presenter_for_path(path)
|
160
|
+
name_space = Curlybars.configuration.presenters_namespace
|
161
|
+
name_spaced_path = File.join(name_space, path)
|
162
|
+
full_class_name = presenter_name_for_path(name_spaced_path)
|
163
|
+
begin
|
164
|
+
full_class_name.constantize
|
165
|
+
rescue NameError
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# The set of view paths that the presenter depends on.
|
171
|
+
#
|
172
|
+
# Examples
|
173
|
+
#
|
174
|
+
# class Posts::ShowPresenter < Curlybars::Presenter
|
175
|
+
# version 2
|
176
|
+
# depends_on 'posts/comment', 'posts/comment_form'
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# Posts::ShowPresenter.dependencies
|
180
|
+
# #=> ['posts/comment', 'posts/comment_form']
|
181
|
+
#
|
182
|
+
# Returns a Set of String view paths.
|
183
|
+
def dependencies
|
184
|
+
# The base presenter doesn't have any dependencies.
|
185
|
+
return SortedSet.new if self == Curlybars::Presenter
|
186
|
+
|
187
|
+
@dependencies ||= SortedSet.new
|
188
|
+
@dependencies.union(superclass.dependencies)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Indicate that the presenter depends a list of other views.
|
192
|
+
#
|
193
|
+
# deps - A list of String view paths that the presenter depends on.
|
194
|
+
#
|
195
|
+
# Returns nothing.
|
196
|
+
def depends_on(*dependencies)
|
197
|
+
@dependencies ||= SortedSet.new
|
198
|
+
@dependencies.merge(dependencies)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Get or set the version of the presenter.
|
202
|
+
#
|
203
|
+
# version - The Integer version that should be set. If nil, no version
|
204
|
+
# is set.
|
205
|
+
#
|
206
|
+
# Returns the current Integer version of the presenter.
|
207
|
+
def version(version = nil)
|
208
|
+
@version = version if version.present?
|
209
|
+
@version || 0
|
210
|
+
end
|
211
|
+
|
212
|
+
# The cache key for the presenter class. Includes all dependencies as
|
213
|
+
# well.
|
214
|
+
#
|
215
|
+
# Returns a String cache key.
|
216
|
+
def cache_key
|
217
|
+
@cache_key ||= compute_cache_key
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def compute_cache_key
|
223
|
+
dependency_cache_keys = dependencies.map do |path|
|
224
|
+
presenter = presenter_for_path(path)
|
225
|
+
if presenter.present?
|
226
|
+
presenter.cache_key
|
227
|
+
else
|
228
|
+
path
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
[name, version, dependency_cache_keys].flatten.join("/")
|
233
|
+
end
|
234
|
+
|
235
|
+
def presents(*args, **options, &block)
|
236
|
+
if options.key?(:default) && block_given?
|
237
|
+
raise ArgumentError, "Cannot provide both `default:` and block"
|
238
|
+
end
|
239
|
+
|
240
|
+
self.presented_names += args.map(&:to_s)
|
241
|
+
|
242
|
+
if options.key?(:default)
|
243
|
+
args.each do |arg|
|
244
|
+
self.default_values = default_values.merge(arg.to_s => options[:default]).freeze
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
return unless block_given?
|
249
|
+
|
250
|
+
args.each do |arg|
|
251
|
+
self.default_blocks = default_blocks.merge(arg.to_s => block).freeze
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def exposes_helper(*methods)
|
256
|
+
methods.each do |method_name|
|
257
|
+
define_method(method_name) do |*args|
|
258
|
+
@_context.public_send(method_name, *args)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
alias_method :exposes_helpers, :exposes_helper
|
264
|
+
end
|
265
|
+
|
266
|
+
private
|
267
|
+
|
268
|
+
class_attribute :presented_names, :default_values, :default_blocks
|
269
|
+
|
270
|
+
self.presented_names = [].freeze
|
271
|
+
self.default_values = {}.freeze
|
272
|
+
self.default_blocks = {}.freeze
|
273
|
+
|
274
|
+
delegate :render, to: :@_context
|
275
|
+
|
276
|
+
# Delegates private method calls to the current view context.
|
277
|
+
#
|
278
|
+
# The view context, an instance of ActionView::Base, is set by Rails.
|
279
|
+
def method_missing(method, *args, &block)
|
280
|
+
@_context.public_send(method, *args, &block)
|
281
|
+
end
|
282
|
+
|
283
|
+
# Tells ruby (and developers) what methods we can accept.
|
284
|
+
def respond_to_missing?(method, include_private = false)
|
285
|
+
@_context.respond_to?(method, false)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|