roger 1.4.6 → 1.5.0
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 +13 -5
- data/.travis.yml +1 -1
- data/CHANGELOG.md +9 -0
- data/doc/templating.md +9 -4
- data/lib/roger/rack/roger.rb +4 -3
- data/lib/roger/release/processors/mockup.rb +25 -42
- data/lib/roger/renderer.rb +294 -0
- data/lib/roger/resolver.rb +118 -37
- data/lib/roger/template.rb +44 -115
- data/lib/roger/template/helpers/capture.rb +4 -2
- data/lib/roger/template/helpers/partial.rb +9 -20
- data/lib/roger/template/helpers/rendering.rb +15 -0
- data/lib/roger/template/template_context.rb +10 -4
- data/lib/roger/version.rb +1 -1
- data/test/project/html/formats/markdown-erb.md.erb +0 -0
- data/test/project/html/formats/preferred.html.erb +0 -0
- data/test/project/html/formats/preferred.json.erb +0 -0
- data/test/project/html/partials/_local.html.erb +1 -0
- data/test/project/html/renderer/file.html.erb +1 -0
- data/test/project/html/renderer/recursive.html.erb +1 -0
- data/test/project/partials/test/_underscored.html.erb +1 -0
- data/test/project/partials/test/deep_recursive.html.erb +1 -0
- data/test/project/partials/test/recursive.html.erb +1 -0
- data/test/unit/release/processors/mockup_test.rb +31 -15
- data/test/unit/renderer/renderer_base_test.rb +123 -0
- data/test/unit/renderer/renderer_content_for_test.rb +50 -0
- data/test/unit/renderer/renderer_helper_test.rb +54 -0
- data/test/unit/renderer/renderer_partial_test.rb +88 -0
- data/test/unit/resolver_test.rb +43 -22
- data/test/unit/template/template_base_test.rb +29 -0
- metadata +39 -11
- data/test/unit/template_test.rb +0 -208
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MTExNTllMmViNTJmMjIwNDJkZmQ3NWIyM2RiNDZmNWMzNDA2MDNiMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MjRlNGI2OWY2NTJmY2YxMTMyYjdkYzUyZTljN2Q5MTk1NGMxNWJhNg==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZDBlYWNhNzc3Zjk2YjMwY2RlYTBiYTZiZTkwYThmZTZmZGZiODNkM2QzMjE5
|
10
|
+
YjdlMDZhY2VhZjFjNDBmMTVhNDQ1YzhkOTg2MjEyZGQ5ZjA4N2M3NGY5YjVl
|
11
|
+
N2YyNDM5NTUyNmZmZTk2ODQyZDE3ZDcyNmQ2MTcxZjVhMDUzMWU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZTcxYWI2MGJlMWU0YTZmYTVmNjAxMDdiOGE1MzAxYzI0ZTFjZmFhMWY1MmQ1
|
14
|
+
YmRhNzMzMjczZDA1NTJiOTY1MjEwN2NhZGVlY2QwYjE1MDVjZWVhYTBmMWJi
|
15
|
+
ODAxZTFmNjNhYzQwMjc0MmYyMjMzNGJlMmJiNjU0NTVmNmRiZTU=
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## Version 1.5.0
|
4
|
+
* Roger won't be tested on Ruby 1.9.x anymore
|
5
|
+
* The way we render templates has been revamped. It is now possible to have multi-pass templates so you can do `.md.erb` which will first be processed by `erb` and then by the `md` tilt template handler.
|
6
|
+
* The way templates are searched has changed to be more predictable. You may notice the change when you have the same filenames with different extensions.
|
7
|
+
* You can now use local partials by just using an underscore as prefix. So `<%= partial('bla') %>` will look for `_bla.*` relative to the current template first and `bla.*` in the partials directory second (current behaviour).
|
8
|
+
* Template recursion will be detected and prevented instead of giving a stack overflow.
|
9
|
+
* Add `render_file` method to renderer and as helper so you can render any template on disk.
|
10
|
+
* Minor doc improvements
|
11
|
+
|
3
12
|
## Version 1.4.6
|
4
13
|
* Allow setting target_path in dir finalizer
|
5
14
|
* Always create target_paths if they don't exist
|
data/doc/templating.md
CHANGED
@@ -113,7 +113,12 @@ The is the rest of the content
|
|
113
113
|
|
114
114
|
## Partials
|
115
115
|
|
116
|
-
Partials are little pieces of template that can be easily reused. You can access the partials throught the `partial("partialpath")` method.
|
116
|
+
Partials are little pieces of template that can be easily reused. You can access the partials throught the `partial("partialpath")` method. The renderer will search for partials:
|
117
|
+
|
118
|
+
* relative to the template currently being rendered with `partialpath` being prefixed with and underscore (so: `_partialpath`).
|
119
|
+
* relative to the partial path that has been set up.
|
120
|
+
|
121
|
+
You can optionally pass variables to the partial by passing a ruby hash of options as a second parameter. This works like this:
|
117
122
|
|
118
123
|
```ruby
|
119
124
|
partial("path/to/partial/relative/to/partials-path", {:key => "value"})
|
@@ -149,14 +154,14 @@ Template helpers allow you to have custom functions in your templates. These fun
|
|
149
154
|
A helper is nothing more than a plain old Ruby module containing a bunch of functions.
|
150
155
|
|
151
156
|
### Registering helpers
|
152
|
-
Registering helpers can best be done in the Rogerfile.
|
157
|
+
Registering helpers can best be done in the Rogerfile. Register this module as following:
|
153
158
|
|
154
159
|
```ruby
|
155
160
|
Roger::Template.register MyHelperModule
|
156
161
|
```
|
157
162
|
|
158
|
-
After registration the helpers are available for use in all templates, partials, layouts, etc.
|
163
|
+
After registration the helpers are available for use in all templates, partials, layouts, etc. See the [template helpers tests](../test/unit/template/template_helper_test.rb) for example usage.
|
159
164
|
|
160
165
|
## Access to Project
|
161
166
|
|
162
|
-
You can access the Roger project with all it's might and glory by accessing `env["roger.project"]`.
|
167
|
+
You can access the Roger project with all it's might and glory by accessing `env["roger.project"]`.
|
data/lib/roger/rack/roger.rb
CHANGED
@@ -3,6 +3,7 @@ require "rack/response"
|
|
3
3
|
require "rack/file"
|
4
4
|
|
5
5
|
require File.dirname(__FILE__) + "/../resolver"
|
6
|
+
require File.dirname(__FILE__) + "/../renderer"
|
6
7
|
|
7
8
|
module Roger
|
8
9
|
module Rack
|
@@ -34,8 +35,8 @@ module Roger
|
|
34
35
|
protected
|
35
36
|
|
36
37
|
def build_response(template_path, env)
|
37
|
-
|
38
|
-
|
38
|
+
renderer = ::Roger::Renderer.new(
|
39
|
+
env,
|
39
40
|
partials_path: @project.partials_path,
|
40
41
|
layouts_path: @project.layouts_path
|
41
42
|
)
|
@@ -43,7 +44,7 @@ module Roger
|
|
43
44
|
::Rack::Response.new do |res|
|
44
45
|
res.headers["Content-Type"] = mime if mime
|
45
46
|
res.status = 200
|
46
|
-
res.write
|
47
|
+
res.write renderer.render(template_path)
|
47
48
|
end
|
48
49
|
end
|
49
50
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../../renderer"
|
2
|
+
|
1
3
|
module Roger::Release::Processors
|
2
4
|
# The Mockup processor that will process all templates
|
3
5
|
class Mockup < Base
|
@@ -37,43 +39,50 @@ module Roger::Release::Processors
|
|
37
39
|
end
|
38
40
|
|
39
41
|
def run_on_file!(file_path, env = {})
|
40
|
-
|
41
|
-
file_path,
|
42
|
-
partials_path: project.partial_path,
|
43
|
-
layouts_path: project.layouts_path
|
44
|
-
)
|
42
|
+
output = run_on_file(file_path, env)
|
45
43
|
|
46
44
|
# Clean up source file
|
47
45
|
FileUtils.rm(file_path)
|
48
46
|
|
49
47
|
# Write out new file
|
50
|
-
File.open(target_path(file_path
|
51
|
-
f.write(
|
48
|
+
File.open(target_path(file_path).to_s, "w") do |f|
|
49
|
+
f.write(output)
|
52
50
|
end
|
53
51
|
end
|
54
52
|
|
55
53
|
# Runs the template on a single file and return processed source.
|
56
|
-
def
|
57
|
-
Roger::
|
58
|
-
|
54
|
+
def run_on_file(file_path, env = {})
|
55
|
+
renderer = Roger::Renderer.new(
|
56
|
+
env.dup,
|
59
57
|
partials_path: project.partial_path,
|
60
58
|
layouts_path: project.layouts_path
|
61
|
-
)
|
59
|
+
)
|
60
|
+
renderer.render(file_path)
|
62
61
|
end
|
63
62
|
|
64
63
|
# Determines the output path for a mockup path with a certain template
|
65
64
|
#
|
66
65
|
# @return [Pathname]
|
67
|
-
def target_path(path
|
68
|
-
parts
|
66
|
+
def target_path(path)
|
67
|
+
parts = File.basename(path.to_s).split(".")
|
68
|
+
path = path.to_s
|
69
69
|
|
70
70
|
# Always return .html directly as it will cause too much trouble otherwise
|
71
71
|
return Pathname.new(path) if parts.last == "html"
|
72
72
|
|
73
|
-
|
74
|
-
|
73
|
+
target_ext = Roger::Renderer.target_extension_for(path)
|
74
|
+
source_ext = Roger::Renderer.source_extension_for(path)
|
75
75
|
|
76
|
-
|
76
|
+
# If there is no target extension
|
77
|
+
return Pathname.new(path) if target_ext.empty?
|
78
|
+
|
79
|
+
# If we have at least one extension
|
80
|
+
if parts.size > 1
|
81
|
+
source_ext_regexp = /#{Regexp.escape(source_ext)}\Z/
|
82
|
+
Pathname.new(path.gsub(source_ext_regexp, target_ext))
|
83
|
+
else
|
84
|
+
Pathname.new(path + "." + target_ext)
|
85
|
+
end
|
77
86
|
end
|
78
87
|
|
79
88
|
protected
|
@@ -97,32 +106,6 @@ module Roger::Release::Processors
|
|
97
106
|
release.log(self, " Env : #{options[:env].inspect}", true)
|
98
107
|
release.log(self, " Files :", true)
|
99
108
|
end
|
100
|
-
|
101
|
-
# Split the path into two parts:
|
102
|
-
# 1. Filename, in an array, split by .
|
103
|
-
# 2. Pathname of directory
|
104
|
-
def split_path(path)
|
105
|
-
[
|
106
|
-
File.basename(path.to_s).split("."),
|
107
|
-
Pathname.new(File.dirname(path.to_s))
|
108
|
-
]
|
109
|
-
end
|
110
|
-
|
111
|
-
def extension_based_on_mime_type(parts, mime_type)
|
112
|
-
# 2. Try to figure out the extension based on the template's mime-type
|
113
|
-
extension = MIME_TYPES_TO_EXTENSION[mime_type]
|
114
|
-
|
115
|
-
# No matching extension, let's return path
|
116
|
-
return parts.join(".") if extension.nil?
|
117
|
-
|
118
|
-
if parts.size > 1
|
119
|
-
# Strip extension and replace with extension
|
120
|
-
(parts[0..-2] << extension).join(".")
|
121
|
-
else
|
122
|
-
# Let's just add the extension
|
123
|
-
(parts << extension).join(".")
|
124
|
-
end
|
125
|
-
end
|
126
109
|
end
|
127
110
|
end
|
128
111
|
Roger::Release::Processors.register(:mockup, Roger::Release::Processors::Mockup)
|
@@ -0,0 +1,294 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/template"
|
2
|
+
require File.dirname(__FILE__) + "/template/template_context"
|
3
|
+
require File.dirname(__FILE__) + "/resolver"
|
4
|
+
|
5
|
+
module Roger
|
6
|
+
# Roger Renderer class
|
7
|
+
#
|
8
|
+
# The renderer will set up an environment so you can consistently render templates
|
9
|
+
# within that environment
|
10
|
+
class Renderer
|
11
|
+
class << self
|
12
|
+
# Register a helper module that should be included in
|
13
|
+
# every template context.
|
14
|
+
def helper(mod)
|
15
|
+
@helpers ||= []
|
16
|
+
@helpers << mod
|
17
|
+
end
|
18
|
+
|
19
|
+
def helpers
|
20
|
+
@helpers || []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Try to infer the final extension of the output file.
|
24
|
+
def target_extension_for(path)
|
25
|
+
if type = MIME::Types[target_mime_type_for(path)].first
|
26
|
+
# Dirty little hack to enforce the use of .html instead of .htm
|
27
|
+
if type.sub_type == "html"
|
28
|
+
"html"
|
29
|
+
else
|
30
|
+
type.extensions.first
|
31
|
+
end
|
32
|
+
else
|
33
|
+
File.extname(path.to_s).sub(/^\./, "")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def source_extension_for(path)
|
38
|
+
parts = File.basename(File.basename(path.to_s)).split(".")
|
39
|
+
if parts.size > 2
|
40
|
+
parts[-2..-1].join(".")
|
41
|
+
else
|
42
|
+
File.extname(path.to_s).sub(/^\./, "")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Try to figure out the mime type based on the Tilt class and if that doesn't
|
47
|
+
# work we try to infer the type by looking at extensions (needed for .erb)
|
48
|
+
def target_mime_type_for(path)
|
49
|
+
mime =
|
50
|
+
mime_type_from_template(path) ||
|
51
|
+
mime_type_from_filename(path) ||
|
52
|
+
mime_type_from_sub_extension(path)
|
53
|
+
|
54
|
+
mime.to_s if mime
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
# Check last template processor default
|
60
|
+
# output mime type
|
61
|
+
def mime_type_from_template(path)
|
62
|
+
templates = Tilt.templates_for(path.to_s)
|
63
|
+
templates.last && templates.last.default_mime_type
|
64
|
+
end
|
65
|
+
|
66
|
+
def mime_type_from_filename(path)
|
67
|
+
MIME::Types.type_for(File.basename(path.to_s)).first
|
68
|
+
end
|
69
|
+
|
70
|
+
# Will get mime_type from source_path extension
|
71
|
+
# but it will only look at the second extension so
|
72
|
+
# .html.erb will look at .html
|
73
|
+
def mime_type_from_sub_extension(path)
|
74
|
+
parts = File.basename(path.to_s).split(".")
|
75
|
+
MIME::Types.type_for(parts[0..-2].join(".")).first if parts.size > 2
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
attr_accessor :data
|
80
|
+
attr_reader :template_nesting
|
81
|
+
|
82
|
+
def initialize(env = {}, options = {})
|
83
|
+
@options = options
|
84
|
+
@context = prepare_context(env)
|
85
|
+
|
86
|
+
@paths = {
|
87
|
+
partials: [@options[:partials_path]].flatten,
|
88
|
+
layouts: [@options[:layouts_path]].flatten
|
89
|
+
}
|
90
|
+
|
91
|
+
# State data. Whenever we render a new template
|
92
|
+
# we need to update:
|
93
|
+
#
|
94
|
+
# - data from front matter
|
95
|
+
# - template_nesting
|
96
|
+
# - current_template
|
97
|
+
@data = {}
|
98
|
+
@template_nesting = []
|
99
|
+
end
|
100
|
+
|
101
|
+
# The render function
|
102
|
+
#
|
103
|
+
# The render function will take care of rendering the right thing
|
104
|
+
# in the right context. It will:
|
105
|
+
#
|
106
|
+
# - Wrap templates with layouts if it's defined in the frontmatter and
|
107
|
+
# load them from the right layout path.
|
108
|
+
# - Render only partials if called from within an existing template
|
109
|
+
#
|
110
|
+
# If you just want to render an arbitrary file, use #render_file instead
|
111
|
+
#
|
112
|
+
# @option options [Hash] :locals
|
113
|
+
# @option options [String] :source
|
114
|
+
def render(path, options = {}, &block)
|
115
|
+
template, layout = template_and_layout_for_render(path, options)
|
116
|
+
|
117
|
+
# Set new current template
|
118
|
+
template_nesting.push(template)
|
119
|
+
|
120
|
+
# Copy data to our data store. A bit clunky; as this should be inherited
|
121
|
+
@data = {}.update(@data).update(template.data)
|
122
|
+
|
123
|
+
# Render the template first so we have access to
|
124
|
+
# it's data in the layout.
|
125
|
+
render_result = template.render(options[:locals] || {}, &block)
|
126
|
+
|
127
|
+
# Wrap it in a layout
|
128
|
+
layout.render do
|
129
|
+
render_result
|
130
|
+
end
|
131
|
+
ensure
|
132
|
+
template_nesting.pop
|
133
|
+
end
|
134
|
+
|
135
|
+
# Render any file on disk. No magic. Just rendering.
|
136
|
+
#
|
137
|
+
# A couple of things to keep in mind:
|
138
|
+
# - The file will be rendered in this rendering context
|
139
|
+
# - Does not have layouts or block style
|
140
|
+
# - When you pass a relative path and we are within another template
|
141
|
+
# it will be relative to that template.
|
142
|
+
#
|
143
|
+
# @options options [Hash] :locals
|
144
|
+
def render_file(path, options = {})
|
145
|
+
pn = Pathname.new(path)
|
146
|
+
|
147
|
+
if pn.relative?
|
148
|
+
# We're explicitly checking for source_path instead of real_source_path
|
149
|
+
# as you could also just have an inline template.
|
150
|
+
if current_template && current_template.source_path
|
151
|
+
pn = (Pathname.new(current_template.source_path).dirname + pn).realpath
|
152
|
+
else
|
153
|
+
err = "Only within another template you can use relative paths"
|
154
|
+
fail ArgumentError, err
|
155
|
+
end
|
156
|
+
else
|
157
|
+
pn = pn.realpath
|
158
|
+
end
|
159
|
+
|
160
|
+
template = template(pn.to_s, nil)
|
161
|
+
template.render(options[:locals] || {})
|
162
|
+
end
|
163
|
+
|
164
|
+
# The current template being rendered
|
165
|
+
def current_template
|
166
|
+
template_nesting.last
|
167
|
+
end
|
168
|
+
|
169
|
+
# The parent template in the nesting.
|
170
|
+
def parent_template
|
171
|
+
template_nesting[-2]
|
172
|
+
end
|
173
|
+
|
174
|
+
protected
|
175
|
+
|
176
|
+
def template_and_layout_for_render(path, options = {})
|
177
|
+
# A previous template has been set so it's a partial
|
178
|
+
# If no previous template is set, we're
|
179
|
+
# at the top level and this means we get to do layouts!
|
180
|
+
template_type = current_template ? :partial : :template
|
181
|
+
template = template(path, options[:source], template_type)
|
182
|
+
|
183
|
+
# Only attempt to load layout for toplevel
|
184
|
+
if !current_template && template.data[:layout]
|
185
|
+
layout = template(template.data[:layout], nil, :layout)
|
186
|
+
else
|
187
|
+
layout = BlankTemplate.new
|
188
|
+
end
|
189
|
+
|
190
|
+
[template, layout]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Will check the template nesting if we haven't already
|
194
|
+
# rendered this path before. If it has we'll throw an argumenteerror
|
195
|
+
def prevent_recursion!(template)
|
196
|
+
# If this template is not a real file it cannot ever conflict.
|
197
|
+
return unless template.real_source_path
|
198
|
+
|
199
|
+
caller_template = template_nesting.detect do |t|
|
200
|
+
t.real_source_path == template.real_source_path
|
201
|
+
end
|
202
|
+
|
203
|
+
# We're good, no recursion!
|
204
|
+
return unless caller_template
|
205
|
+
|
206
|
+
err = "Recursive render detected for '#{template.source_path}'"
|
207
|
+
err += " in '#{current_template.source_path}'"
|
208
|
+
|
209
|
+
fail ArgumentError, err
|
210
|
+
end
|
211
|
+
|
212
|
+
# Will instantiate a Template or throw an ArgumentError
|
213
|
+
# if it could not find the template
|
214
|
+
def template(path, source, type = :template)
|
215
|
+
if source
|
216
|
+
template = Template.new(source, @context, source_path: path)
|
217
|
+
else
|
218
|
+
case type
|
219
|
+
when :partial
|
220
|
+
template_path = find_partial(path)
|
221
|
+
when :layout
|
222
|
+
template_path = find_layout(path)
|
223
|
+
else
|
224
|
+
template_path = path
|
225
|
+
end
|
226
|
+
|
227
|
+
if template_path && File.exist?(template_path)
|
228
|
+
template = Template.open(template_path, @context)
|
229
|
+
else
|
230
|
+
template_not_found!(type, path)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
prevent_recursion!(template)
|
235
|
+
|
236
|
+
template
|
237
|
+
end
|
238
|
+
|
239
|
+
def template_not_found!(type, path)
|
240
|
+
err = "No such #{type} #{path}"
|
241
|
+
err += " in #{@current_template.source_path}" if @current_template
|
242
|
+
fail ArgumentError, err
|
243
|
+
end
|
244
|
+
|
245
|
+
# Find a partial
|
246
|
+
def find_partial(name)
|
247
|
+
current_path, current_ext = current_template_path_and_extension
|
248
|
+
|
249
|
+
# Try to find _ named partials first.
|
250
|
+
# This will alaso search for partials relative to the current path
|
251
|
+
local_name = [File.dirname(name), "_" + File.basename(name)].join("/")
|
252
|
+
resolver = Resolver.new([File.dirname(current_path)] + @paths[:partials])
|
253
|
+
result = resolver.find_template(local_name, prefer: current_ext)
|
254
|
+
|
255
|
+
return result if result
|
256
|
+
|
257
|
+
# Try to look for templates the old way
|
258
|
+
resolver = Resolver.new(@paths[:partials])
|
259
|
+
resolver.find_template(name, prefer: current_ext)
|
260
|
+
end
|
261
|
+
|
262
|
+
def find_layout(name)
|
263
|
+
_, current_ext = current_template_path_and_extension
|
264
|
+
|
265
|
+
resolver = Resolver.new(@paths[:layouts])
|
266
|
+
resolver.find_template(name, prefer: current_ext)
|
267
|
+
end
|
268
|
+
|
269
|
+
def current_template_path_and_extension
|
270
|
+
path = nil
|
271
|
+
extension = nil
|
272
|
+
|
273
|
+
# We want the preferred extension to be the same as ours
|
274
|
+
if current_template
|
275
|
+
path = current_template.source_path
|
276
|
+
extension = self.class.target_extension_for(path)
|
277
|
+
end
|
278
|
+
|
279
|
+
[path, extension]
|
280
|
+
end
|
281
|
+
|
282
|
+
# Will set up a new template context for this renderer
|
283
|
+
def prepare_context(env)
|
284
|
+
context = Roger::Template::TemplateContext.new(self, env)
|
285
|
+
|
286
|
+
# Extend context with all helpers
|
287
|
+
self.class.helpers.each do |mod|
|
288
|
+
context.extend(mod)
|
289
|
+
end
|
290
|
+
|
291
|
+
context
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|