curly-templates 1.0.1 → 2.0.0.beta1
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 +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.
|