serve 0.9.10 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/{History.txt → CHANGELOG.rdoc} +9 -0
- data/{License.txt → LICENSE} +1 -1
- data/{Quickstart.textile → QUICKSTART.rdoc} +61 -80
- data/{README.txt → README.rdoc} +10 -13
- data/Rakefile +11 -13
- data/VERSION +1 -0
- data/bin/serve +6 -0
- data/lib/serve.rb +2 -8
- data/lib/serve/application.rb +13 -6
- data/lib/serve/file_resolver.rb +48 -0
- data/lib/serve/handlers/dynamic_handler.rb +31 -21
- data/lib/serve/handlers/file_type_handler.rb +23 -32
- data/lib/serve/handlers/markdown_handler.rb +1 -1
- data/lib/serve/handlers/redirect_handler.rb +6 -5
- data/lib/serve/handlers/sass_handler.rb +4 -3
- data/lib/serve/rack.rb +57 -0
- data/lib/serve/rails.rb +4 -0
- data/lib/serve/rails/configuration.rb +69 -0
- data/lib/serve/rails/mount.rb +38 -0
- data/lib/serve/rails/routing.rb +25 -0
- data/lib/serve/rails/serve_controller.rb +52 -0
- data/lib/serve/response_cache.rb +170 -0
- data/lib/serve/version.rb +4 -9
- data/lib/serve/webrick/extensions.rb +80 -15
- data/lib/serve/webrick/server.rb +17 -0
- data/lib/serve/webrick/servlet.rb +19 -0
- data/rails/init.rb +6 -0
- data/spec/{serve_application_spec.rb → application_spec.rb} +5 -4
- data/spec/response_cache_spec.rb +248 -0
- data/spec/serve_spec.rb +1 -0
- data/spec/spec_helper.rb +10 -9
- metadata +51 -69
- data.tar.gz.sig +0 -0
- data/Manifest.txt +0 -44
- data/config/hoe.rb +0 -70
- data/config/requirements.rb +0 -17
- data/log/debug.log +0 -0
- data/script/destroy +0 -14
- data/script/generate +0 -14
- data/script/txt2html +0 -74
- data/setup.rb +0 -1585
- data/tasks/deployment.rake +0 -27
- data/tasks/environment.rake +0 -7
- data/tasks/rspec.rake +0 -21
- data/tasks/undefine.rake +0 -5
- data/tasks/website.rake +0 -17
- data/test_project/_layout.haml +0 -12
- data/test_project/erb/_footer.html.erb +0 -2
- data/test_project/erb/_layout.html.erb +0 -28
- data/test_project/erb/index.html.erb +0 -10
- data/test_project/haml/_footer.haml +0 -2
- data/test_project/haml/_layout.haml +0 -20
- data/test_project/haml/index.haml +0 -9
- data/test_project/test.haml +0 -3
- data/test_project/test.html.erb +0 -3
- data/test_project/view_helpers.rb +0 -5
- metadata.gz.sig +0 -0
@@ -1,46 +1,37 @@
|
|
1
1
|
module Serve #:nodoc:
|
2
|
-
class FileTypeHandler
|
3
|
-
|
2
|
+
class FileTypeHandler #:nodoc:
|
3
|
+
def self.handlers
|
4
|
+
@handlers ||= {}
|
5
|
+
end
|
6
|
+
|
4
7
|
def self.extension(*extensions)
|
5
|
-
extensions.each do |
|
6
|
-
|
8
|
+
extensions.each do |ext|
|
9
|
+
FileTypeHandler.handlers[ext] = self
|
7
10
|
end
|
8
11
|
end
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
13
|
+
def self.find(path)
|
14
|
+
if ext = File.extname(path)
|
15
|
+
handlers[ext.sub(/\A\./, '')]
|
16
|
+
end
|
14
17
|
end
|
15
18
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
res.body = parse(data)
|
19
|
+
def initialize(root_path, path)
|
20
|
+
@root_path = root_path
|
21
|
+
@script_filename = File.join(@root_path, path)
|
20
22
|
end
|
21
23
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
rescue StandardError => ex
|
26
|
-
raise
|
27
|
-
rescue Exception => ex
|
28
|
-
@logger.error(ex)
|
29
|
-
raise ::WEBrick::HTTPStatus::InternalServerError, ex.message
|
30
|
-
end
|
24
|
+
def process(request, response)
|
25
|
+
response.headers['content-type'] = content_type
|
26
|
+
response.body = parse(open(@script_filename){|io| io.read })
|
31
27
|
end
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
def content_type
|
38
|
-
'text/html'
|
39
|
-
end
|
40
|
-
|
41
|
-
def parse(string)
|
42
|
-
string.dup
|
43
|
-
end
|
29
|
+
def content_type
|
30
|
+
'text/html'
|
31
|
+
end
|
44
32
|
|
33
|
+
def parse(string)
|
34
|
+
string.dup
|
35
|
+
end
|
45
36
|
end
|
46
37
|
end
|
@@ -2,11 +2,12 @@ module Serve #:nodoc:
|
|
2
2
|
class RedirectHandler < FileTypeHandler #:nodoc:
|
3
3
|
extension 'redirect'
|
4
4
|
|
5
|
-
def process(
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
def process(request, response)
|
6
|
+
url = super.strip
|
7
|
+
unless url =~ %r{^\w[\w\d+.-]*:.*}
|
8
|
+
url = request.protocol + request.host_with_port + url
|
9
|
+
end
|
10
|
+
response.redirect(url, '302')
|
10
11
|
end
|
11
12
|
end
|
12
13
|
end
|
@@ -1,16 +1,17 @@
|
|
1
1
|
module Serve #:nodoc:
|
2
2
|
class SassHandler < FileTypeHandler #:nodoc:
|
3
3
|
extension 'sass'
|
4
|
-
|
4
|
+
|
5
5
|
def parse(string)
|
6
6
|
require 'sass'
|
7
7
|
engine = Sass::Engine.new(string,
|
8
|
+
:load_paths => [@root_path],
|
8
9
|
:style => :expanded,
|
9
10
|
:filename => @script_filename
|
10
11
|
)
|
11
|
-
engine.render
|
12
|
+
engine.render
|
12
13
|
end
|
13
|
-
|
14
|
+
|
14
15
|
def content_type
|
15
16
|
'text/css'
|
16
17
|
end
|
data/lib/serve/rack.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'serve'
|
3
|
+
require 'rack'
|
4
|
+
|
5
|
+
module Serve
|
6
|
+
class Rack
|
7
|
+
def call(env)
|
8
|
+
path = Serve.resolve_file(Dir.pwd, env["PATH_INFO"])
|
9
|
+
return not_found unless path
|
10
|
+
|
11
|
+
ext = File.extname(path)[1..-1]
|
12
|
+
handler = Serve::FileTypeHandler.handlers[ext]
|
13
|
+
return no_handler(ext) unless handler
|
14
|
+
|
15
|
+
res = Response.new
|
16
|
+
handler.new(Dir.pwd, path).process(nil, res)
|
17
|
+
[200, res.headers, res.body]
|
18
|
+
rescue Exception => e
|
19
|
+
return html_response(500, %(
|
20
|
+
<h1>Error!</h1>
|
21
|
+
<h2>#{h(e.message)}</h2>
|
22
|
+
<pre>
|
23
|
+
#{h(e.backtrace.join("\n"))}
|
24
|
+
</pre>))
|
25
|
+
end
|
26
|
+
|
27
|
+
def no_handler(ext)
|
28
|
+
html_response(501, %(
|
29
|
+
<h1>No handler</h1>
|
30
|
+
|
31
|
+
<p>Don't know how to handle resources of type "#{h(ext)}".</p>))
|
32
|
+
end
|
33
|
+
|
34
|
+
def not_found
|
35
|
+
html_response(404, %(
|
36
|
+
<h1>Not Found</h1>
|
37
|
+
|
38
|
+
<p>The requested resource was not found.</p>))
|
39
|
+
end
|
40
|
+
|
41
|
+
def html_response(code, body)
|
42
|
+
[code, {"Content-Type" => "text/html"}, %(<html><head></head><body>#{body}</body></html>)]
|
43
|
+
end
|
44
|
+
|
45
|
+
def h(input)
|
46
|
+
CGI.escapeHTML(input)
|
47
|
+
end
|
48
|
+
|
49
|
+
class Response
|
50
|
+
attr_reader :headers
|
51
|
+
attr_accessor :body
|
52
|
+
def initialize
|
53
|
+
@headers = {}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/serve/rails.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Serve # :nodoc:
|
2
|
+
module Rails
|
3
|
+
class Configuration # :nodoc:
|
4
|
+
attr_reader :mounts, :view_helpers
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
Serve::ResponseCache.defaults.update(
|
8
|
+
:logger => ActionController::Base.logger,
|
9
|
+
:perform_caching => ActionController::Base.perform_caching
|
10
|
+
)
|
11
|
+
|
12
|
+
@mounts = []
|
13
|
+
|
14
|
+
define_find_cache do |request|
|
15
|
+
@default_cache ||= Serve::ResponseCache.new(
|
16
|
+
:directory => File.join(::Rails.root, 'tmp', 'serve_cache')
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def define_find_cache(&block)
|
22
|
+
metaclass.module_eval do
|
23
|
+
define_method :find_cache, &block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def mount(route, root_path)
|
28
|
+
m = Mount.new(route, root_path)
|
29
|
+
@mounts << m
|
30
|
+
yield m if block_given?
|
31
|
+
m
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Answer the cache for the given request. Delegates to the 'find_cache'
|
36
|
+
# block, which defaults to a single cache.
|
37
|
+
#
|
38
|
+
def self.cache(request)
|
39
|
+
configuration.find_cache(request)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.configuration
|
43
|
+
@configuration ||= Configuration.new
|
44
|
+
end
|
45
|
+
|
46
|
+
# The most powerful way to configure Serve::Rails, the configuration is
|
47
|
+
# yielded to the provided block. Multiple calls are cumulative - there is
|
48
|
+
# only one configuration instance.
|
49
|
+
#
|
50
|
+
def self.configure
|
51
|
+
yield configuration
|
52
|
+
end
|
53
|
+
|
54
|
+
# Define the strategy for resolving the cache for a request. This allows
|
55
|
+
# applications to provide logic like cache-per-browser. The default is a
|
56
|
+
# single cache.
|
57
|
+
#
|
58
|
+
def self.find_cache(&block)
|
59
|
+
configuration.define_find_cache(&block)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Mount a route on a directory. This allows an application to have
|
63
|
+
# multiple served directories, each connected to different routes.
|
64
|
+
#
|
65
|
+
def self.mount(route, root_path, &block)
|
66
|
+
configuration.mount(route, root_path, &block)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Serve
|
2
|
+
module Rails
|
3
|
+
class Mount # :nodoc:
|
4
|
+
attr_reader :root_path, :route
|
5
|
+
|
6
|
+
def initialize(route, root)
|
7
|
+
@route, @root_path = route, root
|
8
|
+
@view_helper_module_names = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Appends to the collection of view helpers that will be made availabe
|
12
|
+
# to the DynamicHandler. These should be module names. They will be
|
13
|
+
# constantized in order to allow for re-loading in development mode.
|
14
|
+
#
|
15
|
+
def append_view_helper(module_name)
|
16
|
+
@view_helper_module_names << module_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def connection
|
20
|
+
@route == '/' ? '*path' : "#{@route}/*path"
|
21
|
+
end
|
22
|
+
|
23
|
+
def resolve_path(path)
|
24
|
+
Serve.resolve_file(@root_path, path)
|
25
|
+
end
|
26
|
+
|
27
|
+
def view_helpers
|
28
|
+
helpers = Module.new
|
29
|
+
@view_helper_module_names.each do |module_name|
|
30
|
+
helpers.module_eval do
|
31
|
+
include module_name.constantize
|
32
|
+
end
|
33
|
+
end
|
34
|
+
helpers
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Serve
|
2
|
+
module Rails
|
3
|
+
module Routing
|
4
|
+
|
5
|
+
module MapperExtensions
|
6
|
+
def serve
|
7
|
+
serve_mounts = Serve::Rails.configuration.mounts
|
8
|
+
default_site_path = File.join(::Rails.root, 'site')
|
9
|
+
|
10
|
+
if File.directory?(default_site_path) && !serve_mounts.detect {|m| m.route == '/'}
|
11
|
+
mount('/', default_site_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
serve_mounts.each do |mount|
|
15
|
+
@set.add_route(mount.connection, {
|
16
|
+
:controller => 'serve', :action => 'show',
|
17
|
+
:serve_route => mount.route
|
18
|
+
})
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Serve
|
2
|
+
module Rails
|
3
|
+
|
4
|
+
module ServeController
|
5
|
+
def show
|
6
|
+
response.headers.delete('Cache-Control')
|
7
|
+
cache = Serve::Rails.cache(request)
|
8
|
+
if cache.response_cached?(request.path)
|
9
|
+
cache.update_response(request.path, response, request)
|
10
|
+
else
|
11
|
+
mount = Serve::Rails.configuration.mounts.detect {|m| m.route == params[:serve_route]}
|
12
|
+
if path = mount.resolve_path(params[:path] || '/')
|
13
|
+
handler_class = Serve::FileTypeHandler.find(path)
|
14
|
+
handler = handler_class.new(mount.root_path, path)
|
15
|
+
install_view_helpers(handler, mount.view_helpers) if handler_class == Serve::DynamicHandler
|
16
|
+
handler.process(request, response)
|
17
|
+
cache.cache_response(request.path, response)
|
18
|
+
else
|
19
|
+
render_not_found
|
20
|
+
end
|
21
|
+
end
|
22
|
+
@performed_render = true
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def render_not_found
|
27
|
+
render :text => 'not found', :status => 404
|
28
|
+
end
|
29
|
+
|
30
|
+
# This is a quick solution: We need to install the view helpers defined by
|
31
|
+
# the Rails application after those of the Serve'd app's view helpers, so
|
32
|
+
# that those of the Rails application override them.
|
33
|
+
#
|
34
|
+
# Ideally, we'll work toward moving some of this Rails stuff further up
|
35
|
+
# into Serve, so that a simple Serve'd directory uses almost all of the
|
36
|
+
# same code, like the Configuration.
|
37
|
+
#
|
38
|
+
def install_view_helpers(handler, view_helpers)
|
39
|
+
controller = self
|
40
|
+
handler.metaclass.module_eval do
|
41
|
+
define_method :install_view_helpers do |context|
|
42
|
+
super(context)
|
43
|
+
# Make available to view helpers
|
44
|
+
context.instance_variable_set('@controller', controller)
|
45
|
+
context.extend view_helpers
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# This code and it's tests are adapted from the Radiant CMS project.
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Serve
|
6
|
+
class ResponseCache
|
7
|
+
@@defaults = {
|
8
|
+
:expire_time => 5.minutes,
|
9
|
+
:default_extension => '.yml',
|
10
|
+
:perform_caching => true,
|
11
|
+
:use_x_sendfile => false
|
12
|
+
}
|
13
|
+
cattr_accessor :defaults
|
14
|
+
|
15
|
+
attr_accessor :directory, :expire_time, :default_extension, :perform_caching, :logger, :use_x_sendfile
|
16
|
+
alias :page_cache_directory :directory
|
17
|
+
alias :page_cache_extension :default_extension
|
18
|
+
private :page_cache_directory, :page_cache_extension
|
19
|
+
|
20
|
+
# Creates a ResponseCache object with the specified options.
|
21
|
+
#
|
22
|
+
# Options are as follows:
|
23
|
+
# :directory :: the path to the temporary cache directory
|
24
|
+
# :expire_time :: the number of seconds a cached response is considered valid (defaults to 5 min)
|
25
|
+
# :default_extension :: the extension cached files should use (defaults to '.yml')
|
26
|
+
# :perform_caching :: boolean value that turns caching on or off (defaults to true)
|
27
|
+
# :logger :: the application logging object
|
28
|
+
# :use_x_sendfile :: use X-Sendfile headers to speed up transfer of cached pages (not available on all web servers)
|
29
|
+
#
|
30
|
+
def initialize(options = {})
|
31
|
+
options = options.symbolize_keys.reverse_merge(defaults)
|
32
|
+
self.directory = options[:directory]
|
33
|
+
self.expire_time = options[:expire_time]
|
34
|
+
self.default_extension = options[:default_extension]
|
35
|
+
self.perform_caching = options[:perform_caching]
|
36
|
+
self.logger = options[:logger]
|
37
|
+
self.use_x_sendfile = options[:use_x_sendfile]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Caches a response object for path to disk.
|
41
|
+
def cache_response(path, response)
|
42
|
+
if perform_caching
|
43
|
+
path = clean(path)
|
44
|
+
write_response(path, response)
|
45
|
+
end
|
46
|
+
response
|
47
|
+
end
|
48
|
+
|
49
|
+
# If perform_caching is set to true, updates a response object so that it mirrors the
|
50
|
+
# cached version. The request object is required to perform Last-Modified/If-Modified-Since
|
51
|
+
# checks--it is left optional to allow for backwards compatability.
|
52
|
+
def update_response(path, response, request=nil)
|
53
|
+
if perform_caching
|
54
|
+
path = clean(path)
|
55
|
+
read_response(path, response, request)
|
56
|
+
end
|
57
|
+
response
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns metadata for path.
|
61
|
+
def read_metadata(path)
|
62
|
+
path = clean(path)
|
63
|
+
name = "#{page_cache_path(path)}.yml"
|
64
|
+
if File.exists?(name) and not File.directory?(name)
|
65
|
+
content = File.open(name, "rb") { |f| f.read }
|
66
|
+
metadata = YAML::load(content)
|
67
|
+
metadata if metadata['expires'] >= Time.now
|
68
|
+
end
|
69
|
+
rescue
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns true if a response is cached at the specified path.
|
74
|
+
def response_cached?(path)
|
75
|
+
perform_caching && !!read_metadata(path)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Expires the cached response for the specified path.
|
79
|
+
def expire_response(path)
|
80
|
+
path = clean(path)
|
81
|
+
expire_page(path)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Expires the entire cache.
|
85
|
+
def clear
|
86
|
+
Dir["#{directory}/*"].each do |f|
|
87
|
+
FileUtils.rm_rf f
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
# Ensures that path begins with a slash and remove extra slashes.
|
93
|
+
def clean(path)
|
94
|
+
path = path.gsub(%r{/+}, '/')
|
95
|
+
%r{^/?(.*?)/?$}.match(path)
|
96
|
+
"/#{$1}"
|
97
|
+
end
|
98
|
+
|
99
|
+
# Reads a cached response from disk and updates a response object.
|
100
|
+
def read_response(path, response, request)
|
101
|
+
file_path = page_cache_path(path)
|
102
|
+
if metadata = read_metadata(path)
|
103
|
+
response.headers.merge!(metadata['headers'] || {})
|
104
|
+
if client_has_cache?(metadata, request)
|
105
|
+
response.headers.merge!('Status' => '304 Not Modified')
|
106
|
+
elsif use_x_sendfile
|
107
|
+
response.headers.merge!('X-Sendfile' => "#{file_path}.data")
|
108
|
+
else
|
109
|
+
response.body = File.open("#{file_path}.data", "rb") {|f| f.read}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
response
|
113
|
+
end
|
114
|
+
|
115
|
+
def client_has_cache?(metadata, request)
|
116
|
+
return false unless request
|
117
|
+
request_time = Time.httpdate(request.env["HTTP_IF_MODIFIED_SINCE"]) rescue nil
|
118
|
+
response_time = Time.httpdate(metadata['headers']['Last-Modified']) rescue nil
|
119
|
+
return request_time && response_time && response_time <= request_time
|
120
|
+
end
|
121
|
+
|
122
|
+
# Writes a response to disk.
|
123
|
+
def write_response(path, response)
|
124
|
+
if response.cache_timeout
|
125
|
+
if Time === response.cache_timeout
|
126
|
+
expires = response.cache_timeout
|
127
|
+
else
|
128
|
+
expires = Time.now + response.cache_timeout
|
129
|
+
end
|
130
|
+
else
|
131
|
+
expires = Time.now + self.expire_time
|
132
|
+
end
|
133
|
+
response.headers['Last-Modified'] ||= Time.now.httpdate
|
134
|
+
metadata = {
|
135
|
+
'headers' => response.headers,
|
136
|
+
'expires' => expires
|
137
|
+
}.to_yaml
|
138
|
+
cache_page(metadata, response.body, path)
|
139
|
+
end
|
140
|
+
|
141
|
+
def page_cache_path(path)
|
142
|
+
path = (path.empty? || path == "/") ? "/_site-root" : URI.unescape(path)
|
143
|
+
root_dir = File.expand_path(page_cache_directory)
|
144
|
+
cache_path = File.expand_path(File.join(root_dir, path), root_dir)
|
145
|
+
cache_path if cache_path.index(root_dir) == 0
|
146
|
+
end
|
147
|
+
|
148
|
+
def expire_page(path)
|
149
|
+
return unless perform_caching
|
150
|
+
|
151
|
+
if path = page_cache_path(path)
|
152
|
+
File.delete("#{path}.yml") if File.exists?("#{path}.yml")
|
153
|
+
File.delete("#{path}.data") if File.exists?("#{path}.data")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def cache_page(metadata, content, path)
|
158
|
+
return unless perform_caching
|
159
|
+
|
160
|
+
if path = page_cache_path(path)
|
161
|
+
FileUtils.makedirs(File.dirname(path))
|
162
|
+
#dont want yml without data
|
163
|
+
File.open("#{path}.data", "wb+") { |f| f.write(content) }
|
164
|
+
File.open("#{path}.yml", "wb+") { |f| f.write(metadata) }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
NULL_CACHE = ResponseCache.new(:perform_caching => false)
|
170
|
+
end
|