curly-templates 1.0.1 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -4
- data/README.md +179 -8
- data/Rakefile +1 -1
- data/curly-templates.gemspec +10 -3
- data/lib/curly.rb +4 -4
- data/lib/curly/attribute_parser.rb +69 -0
- data/lib/curly/compiler.rb +77 -47
- data/lib/curly/component_compiler.rb +119 -0
- data/lib/curly/component_parser.rb +13 -0
- data/lib/curly/incomplete_block_error.rb +3 -3
- data/lib/curly/incorrect_ending_error.rb +17 -3
- data/lib/curly/invalid_component.rb +13 -0
- data/lib/curly/presenter.rb +31 -11
- data/lib/curly/scanner.rb +24 -11
- data/spec/attribute_parser_spec.rb +46 -0
- data/spec/compiler/collections_spec.rb +153 -0
- data/spec/compiler_spec.rb +18 -34
- data/spec/component_compiler_spec.rb +160 -0
- data/spec/incorrect_ending_error_spec.rb +13 -0
- data/spec/presenter_spec.rb +27 -10
- data/spec/scanner_spec.rb +22 -13
- data/spec/spec_helper.rb +15 -0
- data/spec/template_handler_spec.rb +1 -1
- metadata +16 -5
- data/lib/curly/invalid_reference.rb +0 -13
data/lib/curly/compiler.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'curly/scanner'
|
2
|
+
require 'curly/component_compiler'
|
3
|
+
require 'curly/component_parser'
|
2
4
|
require 'curly/error'
|
3
|
-
require 'curly/
|
5
|
+
require 'curly/invalid_component'
|
4
6
|
require 'curly/incorrect_ending_error'
|
5
7
|
require 'curly/incomplete_block_error'
|
6
8
|
|
@@ -9,7 +11,7 @@ module Curly
|
|
9
11
|
# Compiles Curly templates into executable Ruby code.
|
10
12
|
#
|
11
13
|
# A template must be accompanied by a presenter class. This class defines the
|
12
|
-
#
|
14
|
+
# components that are valid within the template.
|
13
15
|
#
|
14
16
|
class Compiler
|
15
17
|
# Compiles a Curly template to Ruby code.
|
@@ -17,7 +19,7 @@ module Curly
|
|
17
19
|
# template - The template String that should be compiled.
|
18
20
|
# presenter_class - The presenter Class.
|
19
21
|
#
|
20
|
-
# Raises
|
22
|
+
# Raises InvalidComponent if the template contains a component that is not
|
21
23
|
# allowed.
|
22
24
|
# Raises IncorrectEndingError if a conditional block is not ended in the
|
23
25
|
# correct order - the most recent block must be ended first.
|
@@ -28,7 +30,7 @@ module Curly
|
|
28
30
|
end
|
29
31
|
|
30
32
|
# Whether the Curly template is valid. This includes whether all
|
31
|
-
#
|
33
|
+
# components are available on the presenter class.
|
32
34
|
#
|
33
35
|
# template - The template String that should be validated.
|
34
36
|
# presenter_class - The presenter Class.
|
@@ -42,10 +44,11 @@ module Curly
|
|
42
44
|
false
|
43
45
|
end
|
44
46
|
|
45
|
-
attr_reader :template
|
47
|
+
attr_reader :template
|
46
48
|
|
47
49
|
def initialize(template, presenter_class)
|
48
|
-
@template
|
50
|
+
@template = template
|
51
|
+
@presenter_classes = [presenter_class]
|
49
52
|
end
|
50
53
|
|
51
54
|
def compile
|
@@ -67,6 +70,7 @@ module Curly
|
|
67
70
|
|
68
71
|
<<-RUBY
|
69
72
|
buffer = ActiveSupport::SafeBuffer.new
|
73
|
+
presenters = []
|
70
74
|
#{parts.join("\n")}
|
71
75
|
buffer
|
72
76
|
RUBY
|
@@ -74,74 +78,100 @@ module Curly
|
|
74
78
|
|
75
79
|
private
|
76
80
|
|
77
|
-
def
|
78
|
-
|
81
|
+
def presenter_class
|
82
|
+
@presenter_classes.last
|
79
83
|
end
|
80
84
|
|
81
|
-
def
|
82
|
-
compile_conditional_block "
|
85
|
+
def compile_conditional_block_start(component)
|
86
|
+
compile_conditional_block "if", component
|
83
87
|
end
|
84
88
|
|
85
|
-
def
|
86
|
-
|
87
|
-
|
89
|
+
def compile_inverse_conditional_block_start(component)
|
90
|
+
compile_conditional_block "unless", component
|
91
|
+
end
|
88
92
|
|
89
|
-
|
93
|
+
def compile_collection_block_start(component)
|
94
|
+
name, identifier, attributes = ComponentParser.parse(component)
|
95
|
+
method_call = ComponentCompiler.compile_component(presenter_class, name, identifier, attributes)
|
90
96
|
|
91
|
-
|
92
|
-
|
93
|
-
end
|
97
|
+
as = name.singularize
|
98
|
+
counter = "#{as}_counter"
|
94
99
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
<<-RUBY
|
101
|
-
#{keyword} presenter.#{method}
|
102
|
-
RUBY
|
100
|
+
begin
|
101
|
+
item_presenter_class = presenter_class.presenter_for_name(as)
|
102
|
+
rescue NameError
|
103
|
+
raise Curly::Error,
|
104
|
+
"cannot enumerate `#{component}`, could not find matching presenter class"
|
103
105
|
end
|
106
|
+
|
107
|
+
push_block(name, identifier)
|
108
|
+
@presenter_classes.push(item_presenter_class)
|
109
|
+
|
110
|
+
<<-RUBY
|
111
|
+
presenters << presenter
|
112
|
+
items = Array(#{method_call})
|
113
|
+
items.each_with_index do |item, index|
|
114
|
+
item_options = options.merge(:#{as} => item, :#{counter} => index + 1)
|
115
|
+
presenter = #{item_presenter_class}.new(self, item_options)
|
116
|
+
RUBY
|
104
117
|
end
|
105
118
|
|
106
|
-
def
|
107
|
-
|
119
|
+
def compile_conditional_block(keyword, component)
|
120
|
+
name, identifier, attributes = ComponentParser.parse(component)
|
121
|
+
method_call = ComponentCompiler.compile_conditional(presenter_class, name, identifier, attributes)
|
108
122
|
|
109
|
-
|
110
|
-
|
111
|
-
|
123
|
+
push_block(name, identifier)
|
124
|
+
|
125
|
+
<<-RUBY
|
126
|
+
#{keyword} #{method_call}
|
127
|
+
RUBY
|
128
|
+
end
|
129
|
+
|
130
|
+
def compile_conditional_block_end(component)
|
131
|
+
validate_block_end(component)
|
112
132
|
|
113
133
|
<<-RUBY
|
114
134
|
end
|
115
135
|
RUBY
|
116
136
|
end
|
117
137
|
|
118
|
-
def
|
119
|
-
|
138
|
+
def compile_collection_block_end(component)
|
139
|
+
@presenter_classes.pop
|
140
|
+
validate_block_end(component)
|
120
141
|
|
121
|
-
|
122
|
-
|
123
|
-
|
142
|
+
<<-RUBY
|
143
|
+
end
|
144
|
+
presenter = presenters.pop
|
145
|
+
RUBY
|
146
|
+
end
|
124
147
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
RUBY
|
130
|
-
else
|
131
|
-
code = <<-RUBY
|
132
|
-
presenter.#{method} {|*args| yield(*args) }
|
133
|
-
RUBY
|
134
|
-
end
|
148
|
+
def compile_component(component)
|
149
|
+
name, identifier, attributes = ComponentParser.parse(component)
|
150
|
+
method_call = ComponentCompiler.compile_component(presenter_class, name, identifier, attributes)
|
151
|
+
code = "#{method_call} {|*args| yield(*args) }"
|
135
152
|
|
136
|
-
|
153
|
+
"buffer.concat(#{code.strip}.to_s)"
|
137
154
|
end
|
138
155
|
|
139
156
|
def compile_text(text)
|
140
|
-
|
157
|
+
"buffer.safe_concat(#{text.inspect})"
|
141
158
|
end
|
142
159
|
|
143
160
|
def compile_comment(comment)
|
144
161
|
"" # Replace the content with an empty string.
|
145
162
|
end
|
163
|
+
|
164
|
+
def validate_block_end(component)
|
165
|
+
name, identifier, attributes = ComponentParser.parse(component)
|
166
|
+
last_block = @blocks.pop
|
167
|
+
|
168
|
+
unless last_block == [name, identifier]
|
169
|
+
raise Curly::IncorrectEndingError.new([name, identifier], last_block)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def push_block(name, identifier)
|
174
|
+
@blocks.push([name, identifier])
|
175
|
+
end
|
146
176
|
end
|
147
177
|
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Curly
|
2
|
+
class ComponentCompiler
|
3
|
+
attr_reader :presenter_class, :method
|
4
|
+
|
5
|
+
def initialize(presenter_class, method)
|
6
|
+
@presenter_class, @method = presenter_class, method
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.compile_component(presenter_class, method, argument, attributes)
|
10
|
+
new(presenter_class, method).compile(argument, attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.compile_conditional(presenter_class, method, argument, attributes)
|
14
|
+
if argument && argument.end_with?("?")
|
15
|
+
method += "?"
|
16
|
+
argument = argument[0..-2]
|
17
|
+
end
|
18
|
+
|
19
|
+
unless method.end_with?("?")
|
20
|
+
raise Curly::Error, "conditional components must end with `?`"
|
21
|
+
end
|
22
|
+
|
23
|
+
new(presenter_class, method).compile(argument, attributes)
|
24
|
+
end
|
25
|
+
|
26
|
+
def compile(argument, attributes = {})
|
27
|
+
unless presenter_class.component_available?(method)
|
28
|
+
raise Curly::InvalidComponent.new(method)
|
29
|
+
end
|
30
|
+
|
31
|
+
validate_attributes(attributes)
|
32
|
+
|
33
|
+
code = "presenter.#{method}("
|
34
|
+
|
35
|
+
append_positional_argument(code, argument)
|
36
|
+
append_keyword_arguments(code, argument, attributes)
|
37
|
+
|
38
|
+
code << ")"
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def append_positional_argument(code, argument)
|
44
|
+
if required_identifier?
|
45
|
+
if argument.nil?
|
46
|
+
raise Curly::Error, "`#{method}` requires an identifier"
|
47
|
+
end
|
48
|
+
|
49
|
+
code << argument.inspect
|
50
|
+
elsif optional_identifier?
|
51
|
+
code << argument.inspect unless argument.nil?
|
52
|
+
elsif invalid_signature?
|
53
|
+
raise Curly::Error, "`#{method}` is not a valid component method"
|
54
|
+
elsif !argument.nil?
|
55
|
+
raise Curly::Error, "`#{method}` does not take an identifier"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def append_keyword_arguments(code, argument, attributes)
|
60
|
+
keyword_argument_string = build_keyword_argument_string(attributes)
|
61
|
+
|
62
|
+
unless keyword_argument_string.empty?
|
63
|
+
code << ", " unless argument.nil?
|
64
|
+
code << keyword_argument_string
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def invalid_signature?
|
69
|
+
positional_params = param_types.select {|type| [:req, :opt].include?(type) }
|
70
|
+
positional_params.size > 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def required_identifier?
|
74
|
+
param_types.include?(:req)
|
75
|
+
end
|
76
|
+
|
77
|
+
def optional_identifier?
|
78
|
+
param_types.include?(:opt)
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_keyword_argument_string(kwargs)
|
82
|
+
kwargs.map {|name, value| "#{name}: #{value.inspect}" }.join(", ")
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_attributes(kwargs)
|
86
|
+
kwargs.keys.each do |key|
|
87
|
+
unless attribute_names.include?(key)
|
88
|
+
raise Curly::Error, "`#{method}` does not allow attribute `#{key}`"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
required_attribute_names.each do |key|
|
93
|
+
unless kwargs.key?(key)
|
94
|
+
raise Curly::Error, "`#{method}` is missing the required attribute `#{key}`"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def params
|
100
|
+
@params ||= presenter_class.instance_method(method).parameters
|
101
|
+
end
|
102
|
+
|
103
|
+
def param_types
|
104
|
+
params.map(&:first)
|
105
|
+
end
|
106
|
+
|
107
|
+
def attribute_names
|
108
|
+
@attribute_names ||= params.
|
109
|
+
select {|type, name| [:key, :keyreq].include?(type) }.
|
110
|
+
map {|type, name| name.to_s }
|
111
|
+
end
|
112
|
+
|
113
|
+
def required_attribute_names
|
114
|
+
@required_attribute_names ||= params.
|
115
|
+
select {|type, name| type == :keyreq }.
|
116
|
+
map {|type, name| name.to_s }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'curly/attribute_parser'
|
2
|
+
|
3
|
+
module Curly
|
4
|
+
class ComponentParser
|
5
|
+
def self.parse(component)
|
6
|
+
first, rest = component.split(/\s+/, 2)
|
7
|
+
name, identifier = first.split(".", 2)
|
8
|
+
attributes = AttributeParser.parse(rest)
|
9
|
+
|
10
|
+
[name, identifier, attributes]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module Curly
|
2
2
|
class IncompleteBlockError < Error
|
3
|
-
def initialize(
|
4
|
-
@
|
3
|
+
def initialize(component)
|
4
|
+
@component = component
|
5
5
|
end
|
6
6
|
|
7
7
|
def message
|
8
|
-
"error compiling `{{##{@
|
8
|
+
"error compiling `{{##{@component}}}`: conditional block must be terminated with `{{/#{@component}}}}`"
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -1,11 +1,25 @@
|
|
1
1
|
module Curly
|
2
2
|
class IncorrectEndingError < Error
|
3
|
-
def initialize(
|
4
|
-
@
|
3
|
+
def initialize(actual_block, expected_block)
|
4
|
+
@actual_block, @expected_block = actual_block, expected_block
|
5
5
|
end
|
6
6
|
|
7
7
|
def message
|
8
|
-
"error
|
8
|
+
"compilation error: expected `{{/#{expected_block}}}`, got `{{/#{actual_block}}}`"
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def actual_block
|
14
|
+
present_block(@actual_block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def expected_block
|
18
|
+
present_block(@expected_block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def present_block(block)
|
22
|
+
block.compact.join(".")
|
9
23
|
end
|
10
24
|
end
|
11
25
|
end
|
data/lib/curly/presenter.rb
CHANGED
@@ -6,7 +6,7 @@ module Curly
|
|
6
6
|
# form of simple strings. Each public instance method on the presenter class
|
7
7
|
# can be referenced in a template. When a template is evaluated with a
|
8
8
|
# presenter, the referenced methods will be called with no arguments, and
|
9
|
-
# the returned strings inserted in place of the
|
9
|
+
# the returned strings inserted in place of the components in the template.
|
10
10
|
#
|
11
11
|
# Note that strings that are not HTML safe will be escaped.
|
12
12
|
#
|
@@ -47,7 +47,7 @@ module Curly
|
|
47
47
|
self.class.presented_names.each do |name|
|
48
48
|
value = options.fetch(name) do
|
49
49
|
default_values.fetch(name) do
|
50
|
-
raise ArgumentError.new("required
|
50
|
+
raise ArgumentError.new("required identifier `#{name}` missing")
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
@@ -152,9 +152,28 @@ module Curly
|
|
152
152
|
end
|
153
153
|
end
|
154
154
|
|
155
|
-
|
155
|
+
def presenter_for_name(name)
|
156
|
+
namespace = to_s.split("::")
|
157
|
+
class_name = name.camelcase << "Presenter"
|
158
|
+
|
159
|
+
# Because Rails' autoloading mechanism doesn't work properly with
|
160
|
+
# namespace we need to loop through the namespace ourselves. Ideally,
|
161
|
+
# `X::Y.const_get("Z")` would autoload `X::Z`, but only `X::Y::Z` is
|
162
|
+
# attempted by Rails. This sucks, and hopefully we can find a better
|
163
|
+
# solution in the future.
|
164
|
+
begin
|
165
|
+
full_name = namespace.join("::") << "::" << class_name
|
166
|
+
const_get(full_name)
|
167
|
+
rescue NameError
|
168
|
+
raise if namespace.empty?
|
169
|
+
namespace.pop
|
170
|
+
retry
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Whether a component is available to templates rendered with the presenter.
|
156
175
|
#
|
157
|
-
# Templates
|
176
|
+
# Templates have components which correspond with methods defined on
|
158
177
|
# the presenter. By default, only public instance methods can be
|
159
178
|
# referenced, and any method defined on Curly::Presenter itself cannot be
|
160
179
|
# referenced. This means that methods such as `#cache_key` and #inspect are
|
@@ -162,19 +181,20 @@ module Curly
|
|
162
181
|
#
|
163
182
|
# This policy can be changed by overriding this method in your presenters.
|
164
183
|
#
|
165
|
-
#
|
184
|
+
# name - The String name of the component.
|
166
185
|
#
|
167
186
|
# Returns true if the method can be referenced by a template,
|
168
187
|
# false otherwise.
|
169
|
-
def
|
170
|
-
|
188
|
+
def component_available?(name)
|
189
|
+
available_components.include?(name)
|
171
190
|
end
|
172
191
|
|
173
|
-
# A list of
|
192
|
+
# A list of components available to templates rendered with the presenter.
|
174
193
|
#
|
175
|
-
# Returns an Array of
|
176
|
-
def
|
177
|
-
public_instance_methods - Curly::Presenter.public_instance_methods
|
194
|
+
# Returns an Array of String component names.
|
195
|
+
def available_components
|
196
|
+
methods = public_instance_methods - Curly::Presenter.public_instance_methods
|
197
|
+
methods.map(&:to_s)
|
178
198
|
end
|
179
199
|
|
180
200
|
# The set of view paths that the presenter depends on.
|