sinatra-cache 0.2.3 → 0.3.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.
- data/.gitignore +14 -7
- data/LICENSE +1 -1
- data/README.rdoc +394 -0
- data/Rakefile +66 -23
- data/VERSION +1 -0
- data/lib/sinatra/cache.rb +10 -136
- data/lib/sinatra/cache/helpers.rb +663 -0
- data/lib/sinatra/output.rb +147 -0
- data/lib/sinatra/templates.rb +55 -0
- data/sinatra-cache.gemspec +30 -20
- data/spec/fixtures/apps/base/views/css.sass +2 -0
- data/spec/fixtures/apps/base/views/fragments.erb +11 -0
- data/spec/fixtures/apps/base/views/fragments_shared.erb +11 -0
- data/{test/fixtures → spec/fixtures/apps/base}/views/index.erb +0 -0
- data/spec/fixtures/apps/base/views/layout.erb +9 -0
- data/spec/fixtures/apps/base/views/params.erb +3 -0
- data/spec/sinatra/cache_spec.rb +593 -0
- data/spec/spec_helper.rb +62 -0
- metadata +73 -24
- data/README.md +0 -121
- data/VERSION.yml +0 -4
- data/test/SPECS.rdoc +0 -95
- data/test/cache_test.rb +0 -434
- data/test/fixtures/classic.rb +0 -25
- data/test/fixtures/myapp.rb +0 -36
- data/test/fixtures/myapp_default.rb +0 -37
- data/test/helper.rb +0 -62
data/.gitignore
CHANGED
@@ -1,9 +1,16 @@
|
|
1
|
-
## OS
|
1
|
+
## OS STUFF
|
2
2
|
.DS_Store
|
3
|
+
|
4
|
+
## TM SPECIFIC
|
3
5
|
*.tmproj
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## PROJECT::GENERAL
|
9
|
+
*.sw?
|
10
|
+
coverage
|
11
|
+
rdoc
|
12
|
+
pkg
|
13
|
+
doc
|
14
|
+
|
15
|
+
## PROJECT::SPECIFIC
|
16
|
+
spec/fixtures/public/*
|
data/LICENSE
CHANGED
data/README.rdoc
ADDED
@@ -0,0 +1,394 @@
|
|
1
|
+
= Sinatra::Cache
|
2
|
+
|
3
|
+
A Sinatra Extension that makes Page and Fragment Caching easy.
|
4
|
+
|
5
|
+
|
6
|
+
== IMPORTANT INFORMATION
|
7
|
+
|
8
|
+
<b>This is a completely rewritten extension that basically breaks all previous versions of it.</b>
|
9
|
+
|
10
|
+
So use with care! You have been warned ;-)
|
11
|
+
|
12
|
+
----
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
With that said, on to the real stuff.
|
17
|
+
|
18
|
+
|
19
|
+
== Installation
|
20
|
+
|
21
|
+
# Add RubyGems.org (former Gemcutter) to your RubyGems sources
|
22
|
+
$ gem sources -a http://rubygems.org
|
23
|
+
|
24
|
+
$ (sudo)? gem install sinatra-cache
|
25
|
+
|
26
|
+
== Dependencies
|
27
|
+
|
28
|
+
This Gem depends upon the following:
|
29
|
+
|
30
|
+
=== Runtime:
|
31
|
+
|
32
|
+
* sinatra ( >= 1.0.a )
|
33
|
+
|
34
|
+
|
35
|
+
=== Development & Tests:
|
36
|
+
|
37
|
+
* rspec (>= 1.3.0 )
|
38
|
+
* rack-test (>= 0.5.3)
|
39
|
+
* rspec_hpricot_matchers (>= 0.1.0)
|
40
|
+
* sinatra-tests (>= 0.1.6)
|
41
|
+
* fileutils
|
42
|
+
* sass
|
43
|
+
* ostruct
|
44
|
+
* yaml
|
45
|
+
* json
|
46
|
+
|
47
|
+
|
48
|
+
== Getting Started
|
49
|
+
|
50
|
+
To start caching your app's ouput, just require and register
|
51
|
+
the extension in your sub-classed Sinatra app:
|
52
|
+
|
53
|
+
require 'sinatra/cache'
|
54
|
+
|
55
|
+
class YourApp < Sinatra::Base
|
56
|
+
|
57
|
+
# NB! you need to set the root of the app first
|
58
|
+
set :root, '/path/2/the/root/of/your/app'
|
59
|
+
|
60
|
+
register(Sinatra::Cache)
|
61
|
+
|
62
|
+
set :cache_enabled, true # turn it on
|
63
|
+
|
64
|
+
<snip...>
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
That's more or less it.
|
70
|
+
|
71
|
+
You should now be caching your output by default, in <tt>:production</tt> mode, as long as you use
|
72
|
+
one of Sinatra's render methods:
|
73
|
+
|
74
|
+
erb(), erubis(), haml(), sass(), builder(), etc..
|
75
|
+
|
76
|
+
...or any render method that uses <tt>Sinatra::Templates#render()</tt> as its base.
|
77
|
+
|
78
|
+
|
79
|
+
|
80
|
+
== Configuration Settings
|
81
|
+
|
82
|
+
The default settings should help you get moving quickly, and are fairly common sense based.
|
83
|
+
|
84
|
+
|
85
|
+
==== <tt>:cache_enabled</tt>
|
86
|
+
|
87
|
+
This setting toggles the cache functionality On / Off.
|
88
|
+
Default is: <tt>false</tt>
|
89
|
+
|
90
|
+
|
91
|
+
==== <tt>:cache_environment</tt>
|
92
|
+
|
93
|
+
Sets the environment during which the cache functionality is active.
|
94
|
+
Default is: <tt>:production</tt>
|
95
|
+
|
96
|
+
|
97
|
+
==== <tt>:cache_page_extension</tt>+
|
98
|
+
|
99
|
+
Sets the default file extension for cached files.
|
100
|
+
Default is: <tt>.html</tt>
|
101
|
+
|
102
|
+
|
103
|
+
==== <tt>:cache_output_dir</tt>
|
104
|
+
|
105
|
+
Sets cache directory where the cached files are stored.
|
106
|
+
Default is: == "/path/2/your/app/public"
|
107
|
+
|
108
|
+
Although you can set it to the more ideal '<tt>..public/system/cache/</tt>'
|
109
|
+
if you can get that to work with your webserver setup.
|
110
|
+
|
111
|
+
|
112
|
+
==== <tt>:cache_fragments_output_dir</tt>
|
113
|
+
|
114
|
+
Sets the directory where cached fragments are stored.
|
115
|
+
Default is the '../tmp/cache_fragments/' directory at the root of your app.
|
116
|
+
|
117
|
+
This is for security reasons since you don't really want your cached fragments publically available.
|
118
|
+
|
119
|
+
|
120
|
+
==== <tt>:cache_fragments_wrap_with_html_comments</tt>
|
121
|
+
|
122
|
+
This setting toggles the wrapping of cached fragments in HTML comments. (see below)
|
123
|
+
Default is: <tt>true</tt>
|
124
|
+
|
125
|
+
|
126
|
+
==== <tt>:cache_logging</tt>
|
127
|
+
|
128
|
+
This setting toggles the logging of various cache calls. If the app has access to the <tt>#logger</tt> method,
|
129
|
+
curtesy of Sinatra::Logger[http://github.com/kematzy/sinatra-logger] then it will log there, otherwise logging
|
130
|
+
is silent.
|
131
|
+
|
132
|
+
Default is: <tt>true</tt>
|
133
|
+
|
134
|
+
|
135
|
+
==== <tt>:cache_logging_level</tt>
|
136
|
+
|
137
|
+
Sets the level at which the cache logger should log it's messages.
|
138
|
+
Default is: <tt>:info</tt>
|
139
|
+
|
140
|
+
Available options are: [:fatal, :error, :warn, :info, :debug]
|
141
|
+
|
142
|
+
|
143
|
+
== Basic Page Caching
|
144
|
+
|
145
|
+
By default caching only happens in <tt>:production</tt> mode, and via the Sinatra render methods, erb(), etc,
|
146
|
+
|
147
|
+
So asuming we have the following setup (continued from above)
|
148
|
+
|
149
|
+
|
150
|
+
class YourApp
|
151
|
+
|
152
|
+
<snip...>
|
153
|
+
|
154
|
+
set :cache_output_dir, "/full/path/2/app/root/public/system/cache"
|
155
|
+
|
156
|
+
<snip...>
|
157
|
+
|
158
|
+
get('/') { erb(:index) } # => cached as '../index.html'
|
159
|
+
|
160
|
+
get('/contact') { erb(:contact) } # => cached as '../contact.html'
|
161
|
+
|
162
|
+
# NB! the trailing slash on the URL
|
163
|
+
get('/about/') { erb(:about) } # => cached as '../about/index.html'
|
164
|
+
|
165
|
+
get('/feed.rss') { builder(:feed) } # => cached as '../feed.rss'
|
166
|
+
# NB! uses the extension of the passed URL,
|
167
|
+
# but DOES NOT ensure the format of the content based on the extension provided.
|
168
|
+
|
169
|
+
# complex URL with multiple possible params
|
170
|
+
get %r{/articles/?([\s\w-]+)?/?([\w-]+)?/?([\w-]+)?/?([\w-]+)?/?([\w-]+)?/?([\w-]+)?} do
|
171
|
+
erb(:articles)
|
172
|
+
end
|
173
|
+
# with the '/articles/a/b/c => cached as ../articles/a/b/c.html
|
174
|
+
|
175
|
+
# NB! the trailing slash on the URL
|
176
|
+
# with the '/articles/a/b/c/ => cached as ../articles/a/b/c/index.html
|
177
|
+
|
178
|
+
# CSS caching via Sass # => cached as '.../css/screen.css'
|
179
|
+
get '/css/screen.css' do
|
180
|
+
content_type 'text/css'
|
181
|
+
sass(:'css/screen')
|
182
|
+
end
|
183
|
+
|
184
|
+
# to turn off caching on certain pages.
|
185
|
+
get('/dont/cache/this/page') { erb(:aview, :cache => false) } # => is NOT cached
|
186
|
+
|
187
|
+
|
188
|
+
# NB! any query string params - [ /?page=X&id=y ] - are stripped off and TOTALLY IGNORED
|
189
|
+
# during the caching process.
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
OK, that's about all you need to know about basic Page Caching right there. Read the above example
|
194
|
+
carefully until you understand all the variations.
|
195
|
+
|
196
|
+
|
197
|
+
== Fragment Caching
|
198
|
+
|
199
|
+
If you just need to cache a fragment of a page, then you'd do as follows:
|
200
|
+
|
201
|
+
class YourApp
|
202
|
+
|
203
|
+
set :cache_fragments_output_dir, "/full/path/2/fragments/store/location"
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
Then in your views / layouts add the following:
|
208
|
+
|
209
|
+
<% cache_fragment(:name_of_fragment) do %>
|
210
|
+
# do something worth caching
|
211
|
+
<% end %>
|
212
|
+
|
213
|
+
|
214
|
+
Each fragment is stored in the same directory structure as your request
|
215
|
+
so, if you have a request like this:
|
216
|
+
|
217
|
+
get '/articles/2010/02' ...
|
218
|
+
|
219
|
+
...the cached fragment will be stored as:
|
220
|
+
|
221
|
+
../tmp/cache_fragments/articles/2010/02/< name_of_fragment >.html
|
222
|
+
|
223
|
+
This enables you to use similar names for your fragments or have
|
224
|
+
multiple URLs use the same view / layout.
|
225
|
+
|
226
|
+
|
227
|
+
=== An important limitation
|
228
|
+
|
229
|
+
The fragment caching is dependent upon the final URL, so in the case of
|
230
|
+
a blog, where each article uses the same view, but through different URLs,
|
231
|
+
each of the articles would cache it's own fragment, which is ineffecient.
|
232
|
+
|
233
|
+
To sort-of deal with this limitation I have temporarily added a very hackish
|
234
|
+
'fix' through adding a 2nd parameter (see example below), which will remove the
|
235
|
+
last part of the URL and use the rest of the URL as the stored fragment path.
|
236
|
+
|
237
|
+
So given the URL:
|
238
|
+
|
239
|
+
get '/articles/2010/02/fragment-caching-with-sinatra-cache' ...
|
240
|
+
|
241
|
+
and the following <tt>#cache_fragment</tt> declaration in your view
|
242
|
+
|
243
|
+
<% cache_fragment(:name_of_fragment, :shared) do %>
|
244
|
+
# do something worth caching
|
245
|
+
<% end %>
|
246
|
+
|
247
|
+
...the cached fragment would be stored as:
|
248
|
+
|
249
|
+
../tmp/cache_fragments/articles/2010/02/< name_of_fragment >.html
|
250
|
+
|
251
|
+
Any other URLs with the same URL root, like...
|
252
|
+
|
253
|
+
get '/articles/2010/02/writing-sinatra-extensions' ...
|
254
|
+
|
255
|
+
... would use the same cached fragment.
|
256
|
+
|
257
|
+
|
258
|
+
<b>NB!</b> currently only supports one level, but Your fork might fix that ;-)
|
259
|
+
|
260
|
+
|
261
|
+
== Cache Expiration
|
262
|
+
|
263
|
+
<b>Under development, and not entirely final.</b> See Todo's below for more info.
|
264
|
+
|
265
|
+
|
266
|
+
To expire a cached item - file or fragment you use the :cache_expire() method.
|
267
|
+
|
268
|
+
|
269
|
+
cache_expire('/contact') => expires ../contact.html
|
270
|
+
|
271
|
+
|
272
|
+
# NB! notice the trailing slash
|
273
|
+
cache_expire('/contact/') => expires ../contact/index.html
|
274
|
+
|
275
|
+
|
276
|
+
cache_expire('/feed.rss') => expires ../feed.rss
|
277
|
+
|
278
|
+
|
279
|
+
To expire a cached fragment:
|
280
|
+
|
281
|
+
cache_expire('/some/path', :fragment => :name_of_fragment )
|
282
|
+
|
283
|
+
=> expires ../some/path/:name_of_fragment.html
|
284
|
+
|
285
|
+
|
286
|
+
|
287
|
+
== A few important points to consider
|
288
|
+
|
289
|
+
|
290
|
+
=== The DANGERS of URL query string params
|
291
|
+
|
292
|
+
By default the caching ignores the query string params, but that's not the only problem with query params.
|
293
|
+
|
294
|
+
Let's say you have a URL like this:
|
295
|
+
|
296
|
+
/products/?product_id=111
|
297
|
+
|
298
|
+
and then inside that template [ .../views/products.erb ], you use the <tt>params[:product_id]</tt>
|
299
|
+
param passed in for some purpose.
|
300
|
+
|
301
|
+
<ul>
|
302
|
+
<li>Product ID: <%= params[:product_id] %></li> # => 111
|
303
|
+
...
|
304
|
+
</ul>
|
305
|
+
|
306
|
+
If you cache this URL, then the cached file [ ../cache/products.html ] will be stored with that
|
307
|
+
value embedded. Obviously not ideal for any other similar URLs with different <tt>product_id</tt>'s
|
308
|
+
|
309
|
+
To overcome this issue, use either of these two methods.
|
310
|
+
|
311
|
+
# in your_app.rb
|
312
|
+
|
313
|
+
# turning off caching on this page
|
314
|
+
|
315
|
+
get '/products/' do
|
316
|
+
...
|
317
|
+
erb(:products, :cache => false)
|
318
|
+
end
|
319
|
+
|
320
|
+
# or
|
321
|
+
|
322
|
+
# rework the URLs to something like '/products/111 '
|
323
|
+
|
324
|
+
get '/products/:product_id' do
|
325
|
+
...
|
326
|
+
erb(:products)
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
|
331
|
+
Thats's about all the information you need to know.
|
332
|
+
|
333
|
+
|
334
|
+
== RTFM
|
335
|
+
|
336
|
+
If the above is not clear enough, please check the Specs for a better understanding.
|
337
|
+
|
338
|
+
|
339
|
+
== Errors / Bugs
|
340
|
+
|
341
|
+
If something is not behaving intuitively, it is a bug, and should be reported.
|
342
|
+
Report it here: http://github.com/kematzy/sinatra-cache/issues
|
343
|
+
|
344
|
+
|
345
|
+
== TODOs
|
346
|
+
|
347
|
+
* Improve the fragment caching functionality
|
348
|
+
|
349
|
+
* Decide on how to handle site-wide shared fragments.
|
350
|
+
|
351
|
+
* Make the shared fragments more dynamic or usable
|
352
|
+
|
353
|
+
* Work out how to use the <tt>cache_expire()</tt> functionality in a logical way.
|
354
|
+
|
355
|
+
* Work out and include instructions on how to use a '../public/custom/cache/dir' with Passenger.
|
356
|
+
|
357
|
+
* Write more tests to ensure everything is very solid.
|
358
|
+
|
359
|
+
* Any other improvements you or I can think of.
|
360
|
+
|
361
|
+
|
362
|
+
== Note on Patches/Pull Requests
|
363
|
+
|
364
|
+
* Fork the project.
|
365
|
+
* Make your feature addition or bug fix.
|
366
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
367
|
+
* Commit, do not mess with rakefile, version, or history.
|
368
|
+
* (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
369
|
+
* Send me a pull request. Bonus points for topic branches.
|
370
|
+
|
371
|
+
== Copyright
|
372
|
+
|
373
|
+
Copyright (c) 2009-2010 kematzy. Released under the MIT License.
|
374
|
+
|
375
|
+
See LICENSE for details.
|
376
|
+
|
377
|
+
=== Credits
|
378
|
+
|
379
|
+
Included in this gem is code from <tt>SinatraMore::OutputHelpers</tt>, taken from the
|
380
|
+
<tt>sinatra_more</tt> gem [ http://github.com/nesquena/sinatra_more/ ] by Nathan Esquenazi.
|
381
|
+
|
382
|
+
The code was renamed to Sinatra::Output to prevent any extension clashes.
|
383
|
+
|
384
|
+
Copyright (c) 2009 Nathan Esquenazi. Released under the MIT License.
|
385
|
+
|
386
|
+
|
387
|
+
A big <b>Thank You!</b> goes to rtomayko[http://github/rtomayko], blakemizerany[http://github.com/blakemizerany/]
|
388
|
+
and others working on the Sinatra framework.
|
389
|
+
|
390
|
+
=== Inspirations
|
391
|
+
|
392
|
+
Inspired by code from Rails[http://rubyonrails.com/] & Merb[http://merbivore.com/]
|
393
|
+
and other sources
|
394
|
+
|
data/Rakefile
CHANGED
@@ -5,42 +5,85 @@ begin
|
|
5
5
|
require 'jeweler'
|
6
6
|
Jeweler::Tasks.new do |gem|
|
7
7
|
gem.name = "sinatra-cache"
|
8
|
-
gem.summary = %Q{
|
8
|
+
gem.summary = %Q{A Sinatra Extension that makes Page and Fragment Caching easy.}
|
9
|
+
gem.description = %Q{A Sinatra Extension that makes Page and Fragment Caching easy.}
|
9
10
|
gem.email = "kematzy@gmail.com"
|
10
11
|
gem.homepage = "http://github.com/kematzy/sinatra-cache"
|
11
|
-
gem.description = "Simple Page Caching for Sinatra [www.sinatrarb.com]"
|
12
12
|
gem.authors = ["kematzy"]
|
13
|
+
gem.add_dependency('sinatra', '>=1.0.a')
|
14
|
+
# gem.add_dependency('dependency', '>=x.x.x')
|
15
|
+
gem.add_development_dependency "sinatra-tests", ">= 0.1.6"
|
16
|
+
gem.add_development_dependency "rspec", ">= 1.3.0"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
13
18
|
end
|
14
19
|
rescue LoadError
|
15
|
-
puts "Jeweler not available. Install it with: sudo gem install
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
16
21
|
end
|
17
22
|
|
23
|
+
require 'spec/rake/spectask'
|
24
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_opts = ["--color", "--format", "specdoc", "--require", "spec/spec_helper.rb"]
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.spec_opts = ["--color", "--format", "specdoc", "--require", "spec/spec_helper.rb"]
|
33
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
34
|
+
spec.rcov = true
|
35
|
+
end
|
36
|
+
|
37
|
+
namespace :spec do
|
38
|
+
|
39
|
+
desc "Run all specifications verbosely"
|
40
|
+
Spec::Rake::SpecTask.new(:verbose) do |t|
|
41
|
+
t.libs << "lib"
|
42
|
+
t.spec_opts = ["--color", "--format", "specdoc", "--require", "spec/spec_helper.rb"]
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Run specific spec verbosely (SPEC=/path/2/file)"
|
46
|
+
Spec::Rake::SpecTask.new(:select) do |t|
|
47
|
+
t.libs << "lib"
|
48
|
+
t.spec_files = [ENV["SPEC"]]
|
49
|
+
t.spec_opts = ["--color", "--format", "specdoc", "--require", "spec/spec_helper.rb"]
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
task :spec => :check_dependencies
|
55
|
+
|
56
|
+
task :default => :spec
|
57
|
+
|
18
58
|
require 'rake/rdoctask'
|
19
59
|
Rake::RDocTask.new do |rdoc|
|
60
|
+
version = File.exist?('VERSION') ? IO.read('VERSION').chomp : "[Unknown]"
|
61
|
+
|
20
62
|
rdoc.rdoc_dir = 'rdoc'
|
21
|
-
rdoc.title =
|
22
|
-
rdoc.options << '--line-numbers' << '--inline-source'
|
63
|
+
rdoc.title = "Sinatra::Cache v#{version}"
|
23
64
|
rdoc.rdoc_files.include('README*')
|
24
65
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
25
66
|
end
|
26
67
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
68
|
+
desc 'Build the rdoc HTML Files'
|
69
|
+
task :docs do
|
70
|
+
version = File.exist?('VERSION') ? IO.read('VERSION').chomp : "[Unknown]"
|
71
|
+
|
72
|
+
sh "sdoc -N --title 'Sinatra::Cache v#{version}' lib/ README.rdoc"
|
32
73
|
end
|
33
74
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
75
|
+
namespace :docs do
|
76
|
+
|
77
|
+
desc 'Remove rdoc products'
|
78
|
+
task :remove => [:clobber_rdoc]
|
79
|
+
|
80
|
+
desc 'Force a rebuild of the RDOC files'
|
81
|
+
task :rebuild => [:rerdoc]
|
82
|
+
|
83
|
+
desc 'Build docs, and open in browser for viewing (specify BROWSER)'
|
84
|
+
task :open => [:docs] do
|
85
|
+
browser = ENV["BROWSER"] || "safari"
|
86
|
+
sh "open -a #{browser} doc/index.html"
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|