crawlable 0.0.1.5 → 0.0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,21 +8,73 @@ Super DRY Sitemaps for Rails and Sinatra Apps (works on Heroku!)
8
8
 
9
9
  sudo gem install crawlable
10
10
 
11
- ### Setup (config/sitemap.rb)
11
+ ### Sitemap (`config/sitemap.rb`)
12
12
 
13
- Sitemap do |sitemap|
13
+ Sitemap "http://www.example.com" do
14
+ link articles_path, :priority => 0.7, :changes => 'daily'
15
+
16
+ Post.all.each do |a|
17
+ link articles_path(a), :updated_at => a.updated_at do
18
+ image images_path(a.featured_image)
19
+ end
20
+ end
21
+ end
22
+
23
+ #### Result
14
24
 
15
- sitemap.add posts_path, :priority => 1, :changes => 'daily'
25
+ <?xml version="1.0"?>
26
+ <urlset>
27
+ <url>
28
+ <loc>/articles</loc>
29
+ <lastmod>2010-06-20T09:38:26+00:00</lastmod>
30
+ <changefreq>daily</changefreq>
31
+ <priority>0.7</priority>
32
+ </url>
33
+ <url>
34
+ <loc>/articles/title-0</loc>
35
+ <lastmod>2010-06-20T09:38:26+00:00</lastmod>
36
+ <changefreq>weekly</changefreq>
37
+ <priority>0.5</priority>
38
+ </url>
39
+ <url>
40
+ <loc>/articles/title-1</loc>
41
+ <lastmod>2010-06-20T09:38:26+00:00</lastmod>
42
+ <changefreq>weekly</changefreq>
43
+ <priority>0.5</priority>
44
+ </url>
45
+ ...
46
+ </urlset>
47
+
48
+ ### Feed
16
49
 
17
- Post.all.each do |post|
18
- sitemap.add posts_path(post), :last_modified => post.updated_at
50
+ Feed do
51
+ title "My RSS Feed"
52
+ author "Lance Pollard"
53
+ description "Something nice and tidy"
54
+
55
+ Post.all.each do |a|
56
+ entry "/posts/#{a.to_param}", :updated_at => a.updated_at, :title => a.title
19
57
  end
20
-
21
58
  end
22
59
 
23
60
  ## Features
24
61
 
25
62
  - Works on Heroku
63
+ - Pings Google, Bing, Yahoo!, and Ask whenever anything changes
26
64
 
27
65
  ## Alternatives
28
66
 
67
+ - [SitemapGenerator](http://github.com/kjvarga/sitemap_generator)
68
+ - [Sitemap](http://github.com/queso/sitemap)
69
+ - [BigSitemap](http://github.com/alexrabarts/big_sitemap)
70
+ - [Sitemap (diff than above)](http://github.com/flyerhzm/sitemap)
71
+ - [SitemapGenerator (diff than above)](http://github.com/christianhellsten/sitemap-generator)
72
+ - [Sitemapper](http://github.com/milk-it/sitemapper)
73
+
74
+ ## Resources
75
+
76
+ - [Official Sitemap Protocol](http://sitemaps.org/protocol.php)
77
+ - [Official RSS Feed Spec](http://cyber.law.harvard.edu/rss/rss.html)
78
+ - [Sitemap Engine List](http://en.wikipedia.org/wiki/Sitemap_index)
79
+ - [Yahoo Site Explorer](http://developer.yahoo.com/search/siteexplorer/V1/updateNotification.html)
80
+ - [Comparison of Feed Aggregators](http://en.wikipedia.org/wiki/Comparison_of_feed_aggregators)
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
5
5
  spec = Gem::Specification.new do |s|
6
6
  s.name = "crawlable"
7
7
  s.authors = ["Lance Pollard"]
8
- s.version = "0.0.1.5"
8
+ s.version = "0.0.1.6"
9
9
  s.summary = "Crawlable: Super DRY Sitemaps for Rails and Sinatra Apps"
10
10
  s.homepage = "http://github.com/viatropos/crawlable"
11
11
  s.email = "lancejpollard@gmail.com"
@@ -14,6 +14,7 @@ spec = Gem::Specification.new do |s|
14
14
  s.rubyforge_project = "crawlable"
15
15
  s.platform = Gem::Platform::RUBY
16
16
  s.files = %w(README.markdown Rakefile init.rb MIT-LICENSE) + Dir["{lib,rails,test}/**/*"] - Dir["test/tmp"]
17
+ s.add_dependency("nokogiri", ">= 1.4.1")
17
18
  s.require_path = "lib"
18
19
  end
19
20
 
@@ -1,22 +1,20 @@
1
1
  require 'rubygems'
2
2
  require 'active_support'
3
3
  require 'active_record'
4
+ require 'nokogiri'
5
+ require 'open-uri'
4
6
 
5
7
  this = File.dirname(__FILE__)
6
- Dir["#{this}/crawlable/*"].each { |c| require c }
7
-
8
- class Settings
9
- include Cockpit::Configuration
10
- end
8
+ Dir["#{this}/crawlable/*"].each { |c| require c if File.extname(c) == ".rb" }
11
9
 
12
10
  def Sitemap(*args, &block)
13
- Sitemap.configure(*args, &block)
11
+ Crawlable::Sitemap.define!(*args, &block)
14
12
  end
15
13
 
16
- def Crawlable(*args, &block)
17
- Sitemap(*args, &block)
14
+ def Feed(*args, &block)
15
+ Crawlable::Feed.define!(*args, &block)
18
16
  end
19
17
 
20
- def Crawl(*args, &block)
21
- Sitemap(*args, &block)
22
- end
18
+ def Crawlable(type, *args, &block)
19
+ type.to_s.constantize(*args, &block)
20
+ end
@@ -0,0 +1,168 @@
1
+ module Crawlable
2
+ class Feed
3
+
4
+ class << self
5
+ attr_accessor :options, :call_options
6
+
7
+ def define!(*args, &block)
8
+ self.options = args.extract_options!#.merge(:run => args.shift)
9
+
10
+ instance_eval(&block) if block_given?
11
+ end
12
+
13
+ def parse!(path, options = {})
14
+ path ||= File.join(::Rails.root, 'config/initializers/feeds.rb')
15
+ self.call_options = options.symbolize_keys
16
+ eval(IO.read(path))
17
+ self.call_options = nil
18
+ end
19
+
20
+ def method_missing(meth, *args, &block)
21
+ if block_given?
22
+ if !self.call_options.blank?
23
+ if call_options.has_key?(meth.to_sym)
24
+ self.new(meth, *args, &block)
25
+ else
26
+ super(meth, *args, &block)
27
+ end
28
+ else
29
+ self.new(meth, *args, &block)
30
+ end
31
+ else
32
+ super(meth, *args, &block)
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ if defined?(::Rails)
39
+ if ActionPack::VERSION::MAJOR == 3
40
+ include ::Rails.application.routes.url_helpers
41
+ else
42
+ require 'action_controller'
43
+ include ActionController::UrlWriter
44
+ end
45
+ end
46
+
47
+ attr_accessor :title, :url, :description, :master, :copyright, :updated_at
48
+
49
+ def initialize(*args, &block)
50
+ options = args.extract_options!
51
+
52
+ name = args.shift
53
+
54
+ options.each do |k, v|
55
+ self.send(k, v) if self.respond_to?(k)
56
+ end
57
+
58
+ instance_eval(&block)
59
+ end
60
+
61
+ def copyright(string = nil)
62
+ @copyright = string unless string.nil?
63
+ @copyright
64
+ end
65
+
66
+ def title(string = nil)
67
+ @title = string unless string.nil?
68
+ @title
69
+ end
70
+
71
+ def description(string = nil)
72
+ @description = string unless string.nil?
73
+ @description
74
+ end
75
+
76
+ def url(string = nil)
77
+ @url = string unless string.nil?
78
+ @url
79
+ end
80
+
81
+ def author(string = nil)
82
+ @author = string unless string.nil?
83
+ @author
84
+ end
85
+
86
+ def master(string = nil)
87
+ @master = string unless string.nil?
88
+ @master
89
+ end
90
+
91
+ def entries
92
+ @entries ||= []
93
+ end
94
+
95
+ def entry(path, *args, &block)
96
+ options = args.extract_options!
97
+
98
+ result = options.dup
99
+ result.merge!(:url => path)
100
+
101
+ self.entries.push(result)
102
+ end
103
+
104
+ def to_rss
105
+ builder = Nokogiri::XML::Builder.new do |xml|
106
+ xml.rss "xmlns:dc" => "http://purl.org/dc/elements/1.1/" do
107
+ xml.channel do
108
+ xml.title self.title
109
+ xml.link self.url
110
+ xml.description self.description
111
+
112
+ self.entries.each do |entry|
113
+ puts entry.inspect
114
+ xml.item do
115
+ xml.title entry[:title]
116
+ xml.link entry[:url]
117
+ xml.description entry[:description]
118
+ xml.guid entry[:guid] || entry[:url]
119
+ xml.author entry[:author] unless entry[:author].blank?
120
+ entry[:categories].each do |category|
121
+ xml.category category
122
+ end unless entry[:categories].blank?
123
+ xml.comments entry[:comments] unless entry[:comments].blank?
124
+ xml.enclosure(
125
+ :url => entry[:asset],
126
+ :length => IO.size(entry[:asset]).to_s,
127
+ :type => entry[:asset_type]
128
+ ) unless entry[:asset].blank?
129
+ xml.webMaster self.master unless self.master.blank?
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ builder.to_xml
136
+ end
137
+
138
+ def to_atom
139
+ builder = Nokogiri::XML::Builder.new do |xml|
140
+ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
141
+ xml.title self.title
142
+ xml.link "rel" => "self", "href" => url_for(:only_path => false, :controller => 'feeds', :action => 'atom')
143
+ xml.link "rel" => "alternate", "href" => url_for(:only_path => false, :controller => 'posts')
144
+ xml.id url_for(:only_path => false, :controller => 'posts')
145
+ xml.updated self.updated_at.strftime "%Y-%m-%dT%H:%M:%SZ" if self.updated_at
146
+ xml.author { xml.name self.author }
147
+
148
+ self.entries.each do |entries|
149
+ xml.entry do
150
+ xml.title entries.title
151
+ xml.link "rel" => "alternate", "href" => url_for(:only_path => false, :controller => 'posts', :action => 'show', :id => entries.id)
152
+ xml.id url_for(:only_path => false, :controller => 'posts', :action => 'show', :id => entries.id)
153
+ xml.updated entries.updated_at.strftime "%Y-%m-%dT%H:%M:%SZ"
154
+ xml.author { xml.name entries.author.name }
155
+ xml.summary "Post summary"
156
+ xml.content "type" => "html" do
157
+ #xml.text! render(:partial => "posts/post", :post => post)
158
+ end
159
+ end
160
+ end
161
+
162
+ end
163
+ end
164
+ builder.to_xml
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,25 @@
1
+ module Crawlable
2
+ class Rack
3
+
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if ENV["HEROKU"]
10
+ return sitmap if env['REQUEST_PATH'] =~ /\/sitemap\.xml/i
11
+ end
12
+ @app.call(env)
13
+ end
14
+
15
+ def sitemap
16
+ file = File.join(heroku_writable_directory, "sitemap.xml.gz")
17
+ [200, { 'Cache-Control' => 'public, max-age=86400', 'Content-Length' => File.size(file).to_s, 'Content-Type' => 'text/xml' }, IO.read(file)]
18
+ end
19
+
20
+ def heroku_writable_directory
21
+ "#{Rails.root}/tmp"
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,195 @@
1
+ module Crawlable
2
+ class Sitemap
3
+
4
+ class << self
5
+ attr_accessor :instance
6
+
7
+ def define!(*args, &block)
8
+ self.instance = self.new(*args, &block)
9
+ end
10
+
11
+ def parse!(path)
12
+ path ||= File.join(::Rails.root, 'config/sitemap.rb')
13
+ eval(IO.read(path))
14
+ end
15
+
16
+ def write(to, compress = false)
17
+ self.instance.write(to, compress)
18
+ end
19
+
20
+ def process(from, to, compress = false, &block)
21
+ parse!(from)
22
+ write(to, compress)
23
+ end
24
+
25
+ def inspect
26
+ self.instance.inspect
27
+ end
28
+
29
+ def to_xml
30
+ self.instance.to_xml
31
+ end
32
+
33
+ def clear
34
+ self.instance.clear
35
+ self.instance = nil
36
+ end
37
+
38
+ end
39
+
40
+ attr_accessor :links, :host, :ping, :yahoo_app_id
41
+
42
+ def initialize(*args, &block)
43
+ self.host = args.shift
44
+ raise "Please define a host: 'Sitemap 'http://my-site.com' do ..." if self.host.blank?
45
+ options = args.extract_options!
46
+
47
+ options.each do |k, v|
48
+ self.send(k, v) if self.respond_to?(k)
49
+ end
50
+
51
+ instance_eval(&block)
52
+ end
53
+
54
+ def yahoo_app_id(string = nil)
55
+ @yahoo_app_id = string unless string.nil?
56
+ @yahoo_app_id
57
+ end
58
+
59
+ def links
60
+ @links ||= []
61
+ end
62
+
63
+ def host(*args)
64
+ @host = args unless args.empty?
65
+ @host
66
+ end
67
+
68
+ def ping(*args)
69
+ @ping = args unless args.empty?
70
+ @ping
71
+ end
72
+
73
+ def sitemap_path(string = nil)
74
+ @sitemap_path = string || "public/sitemap.xml"
75
+ end
76
+
77
+ def link(path, *args, &block)
78
+ options = args.extract_options!
79
+ options.assert_valid_keys(:priority, :changes, :updated_at, :host)
80
+ options.reverse_merge!(
81
+ :priority => 0.5,
82
+ :changes => 'monthly',
83
+ :updated_at => Time.now,
84
+ :host => self.host
85
+ )
86
+
87
+ result = {
88
+ :host => options[:host],
89
+ :path => path,
90
+ :url => URI.join(options[:host], path).to_s,
91
+ :priority => options[:priority],
92
+ :changes => options[:changes],
93
+ :updated_at => options[:updated_at],
94
+ :images => []
95
+ }
96
+
97
+ self.links.push(result)
98
+
99
+ instance_eval(&block) if block_given?
100
+
101
+ result
102
+ end
103
+
104
+ def image(path, *args, &block)
105
+ options = args.extract_options!
106
+ options.assert_valid_keys(:priority, :changes, :updated_at, :host)
107
+
108
+ result = {
109
+ :path => path,
110
+ :caption => options[:caption],
111
+ :geo_location => options[:geo_location],
112
+ :title => options[:title],
113
+ :license => options[:license]
114
+ }
115
+
116
+ self.links.last[:images].push(result)
117
+ end
118
+
119
+ def w3c_date(date)
120
+ date.utc.strftime("%Y-%m-%dT%H:%M:%S+00:00")
121
+ end
122
+
123
+ def to_xml
124
+ namespaces = {
125
+ "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9",
126
+ "xmlns:image" => "http://www.google.com/schemas/sitemap-image/1.1"
127
+ }
128
+ builder = Nokogiri::XML::Builder.new do |xml|
129
+ xml.urlset(namespaces) do
130
+ self.links.each do |link|
131
+ xml.url do
132
+ xml.loc link[:path]
133
+ xml.lastmod w3c_date(link[:updated_at]) if link[:updated_at]
134
+ xml.changefreq link[:changes] if link[:changes]
135
+ xml.priority link[:priority] if link[:priority]
136
+ link[:images].each do |image|
137
+ xml["image"].image do
138
+ xml["image"].loc image[:path]
139
+ xml["image"].caption image[:caption] if image[:caption]
140
+ xml["image"].geo_location image[:geo_location] if image[:geo_location]
141
+ xml["image"].title image[:title] if image[:title]
142
+ xml["image"].license image[:license] if image[:license]
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ builder.to_xml
150
+ end
151
+
152
+ def write(path, compress)
153
+ to = path
154
+ if compress
155
+ to << ".gz" unless File.extname(path) == ".gz"
156
+ File.open(to, 'wb') do |file|
157
+ gz = Zlib::GzipWriter.new(file)
158
+ gz.write to_xml
159
+ gz.close
160
+ end
161
+ else
162
+ File.open(to, 'wb') do |file|
163
+ file.puts to_xml
164
+ end
165
+ end
166
+ end
167
+
168
+ def notify
169
+ engines = {
170
+ :google => "http://www.google.com/webmasters/sitemaps/ping?sitemap=#{path}",
171
+ :yahoo => "http://search.yahooapis.com/SiteExplorerService/V1/ping?sitemap=#{path}&appid=#{yahoo_app_id}",
172
+ :ask => "http://submissions.ask.com/ping?sitemap=#{path}",
173
+ :bing => "http://www.bing.com/webmaster/ping.aspx?siteMap=#{path}",
174
+ :sitemap_writer => "http://www.sitemapwriter.com/notify.php?crawler=all&url=#{path}"
175
+ }
176
+ engines.each do |engine, link|
177
+ begin
178
+ open(link)
179
+ puts "Successful ping of #{engine.to_s.titleize}"
180
+ rescue Timeout::Error, StandardError => e
181
+ puts "Ping failed for #{engine.to_s.titleize}: #{e.inspect}"
182
+ end
183
+ end
184
+ end
185
+
186
+ def clear
187
+ @links = nil
188
+ end
189
+
190
+ def inspect
191
+ "<Sitemap @host='#{host.to_s}' @sitemap_path='#{sitemap_path.to_s}' @ping='#{ping.inspect}' @links='#{links.inspect}'/>"
192
+ end
193
+
194
+ end
195
+ end
@@ -0,0 +1,9 @@
1
+ module Crawlable
2
+ class Engine < Rails::Engine
3
+
4
+ initializer "authlogic_connect.authentication_hook" do |app|
5
+ app.middleware.use Crawlable::Rack
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ begin
2
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
3
+ rescue ArgumentError
4
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
5
+ end
6
+
7
+ ActiveRecord::Base.configurations = true
8
+
9
+ ActiveRecord::Schema.define(:version => 1) do
10
+
11
+ create_table :posts, :force => true do |t|
12
+ t.string :title
13
+ t.timestamps
14
+ end
15
+
16
+ end
@@ -0,0 +1,5 @@
1
+ class Post < ActiveRecord::Base
2
+ def to_param
3
+ self.title.downcase.gsub(/\s+/, "-")
4
+ end
5
+ end
File without changes
@@ -0,0 +1,18 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class FeedTest < ActiveSupport::TestCase
4
+
5
+ context "Feed" do
6
+
7
+ setup do
8
+ create_posts
9
+ load_feed
10
+ end
11
+
12
+ should "create feed" do
13
+ puts Crawlable::Feed.to_rss
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -9,6 +9,10 @@ require 'active_record/fixtures'
9
9
  require 'shoulda'
10
10
  require 'shoulda/active_record'
11
11
 
12
+ this = File.expand_path(File.dirname(__FILE__))
13
+ require File.expand_path(File.join(this, '/../lib/crawlable'))
14
+
15
+ Dir["#{this}/lib/*"].each { |c| require c if File.extname(c) == ".rb" }
12
16
  require File.expand_path(File.join(File.dirname(__FILE__), '/../lib/crawlable'))
13
17
 
14
18
  ActiveRecord::Base.class_eval do
@@ -16,3 +20,42 @@ ActiveRecord::Base.class_eval do
16
20
  all.map(&:destroy)
17
21
  end
18
22
  end
23
+
24
+ ActiveSupport::TestCase.class_eval do
25
+
26
+ def create_posts(many = 10)
27
+ many.times do |i|
28
+ Post.create!(:title => "title-#{i.to_s}")
29
+ end
30
+ end
31
+
32
+ def load_sitemap
33
+ Sitemap "http://www.example.com" do
34
+ ping :yahoo, :google
35
+
36
+ link "/posts", :priority => 0.7, :changes => 'daily' do
37
+ 3.times do |i|
38
+ image "/images/#{i}"
39
+ end
40
+ end
41
+
42
+ Post.all.each do |a|
43
+ link "/posts/#{a.to_param}", :updated_at => a.updated_at
44
+ end
45
+ end
46
+ end
47
+
48
+ def load_feed
49
+ Feed do
50
+ posts do
51
+ title "My RSS Feed"
52
+ author "Lance Pollard"
53
+ description "Something nice and tidy"
54
+
55
+ Post.all.each do |a|
56
+ entry "/posts/#{a.to_param}", :updated_at => a.updated_at, :title => a.title
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,8 +4,20 @@ class SitemapTest < ActiveSupport::TestCase
4
4
 
5
5
  context "Sitemap" do
6
6
 
7
+ setup do
8
+ create_posts
9
+ load_sitemap
10
+ end
7
11
 
12
+ should "create sitemap" do
13
+ Crawlable::Sitemap.write("test/sitemap.xml")
14
+ assert File.exists?("test/sitemap.xml.gz")
15
+ end
16
+
17
+ teardown do
18
+ File.delete("test/sitemap.xml.gz") if File.exists?("test/sitemap.xml.gz")
19
+ end
8
20
 
9
21
  end
10
22
 
11
- end
23
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crawlable
3
3
  version: !ruby/object:Gem::Version
4
- hash: 65
4
+ hash: 71
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
9
  - 1
10
- - 5
11
- version: 0.0.1.5
10
+ - 6
11
+ version: 0.0.1.6
12
12
  platform: ruby
13
13
  authors:
14
14
  - Lance Pollard
@@ -16,10 +16,25 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2010-06-19 00:00:00 -07:00
19
+ date: 2010-07-02 00:00:00 -07:00
20
20
  default_executable:
21
- dependencies: []
22
-
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: nokogiri
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 5
31
+ segments:
32
+ - 1
33
+ - 4
34
+ - 1
35
+ version: 1.4.1
36
+ type: :runtime
37
+ version_requirements: *id001
23
38
  description: Super DRY Sitemaps for Rails and Sinatra Apps
24
39
  email: lancejpollard@gmail.com
25
40
  executables: []
@@ -33,9 +48,16 @@ files:
33
48
  - Rakefile
34
49
  - init.rb
35
50
  - MIT-LICENSE
36
- - lib/crawlable/crawlable.rb
51
+ - lib/crawlable/feed.rb
52
+ - lib/crawlable/rack.rb
53
+ - lib/crawlable/sitemap.rb
37
54
  - lib/crawlable.rb
55
+ - lib/engine.rb
38
56
  - rails/init.rb
57
+ - test/lib/_database.rb
58
+ - test/lib/post.rb
59
+ - test/lib/routes.rb
60
+ - test/test_feed.rb
39
61
  - test/test_helper.rb
40
62
  - test/test_sitemap.rb
41
63
  has_rdoc: true
@@ -1,12 +0,0 @@
1
- module Crawlable
2
-
3
- def self.included(base)
4
- base.extend ClassMethods
5
- end
6
-
7
- module ClassMethods
8
- def acts_as_crawlable(*args, &block)
9
- end
10
- end
11
-
12
- end