harbor 0.16.1
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/Rakefile +76 -0
- data/bin/harbor +7 -0
- data/lib/harbor.rb +17 -0
- data/lib/harbor/accessor_injector.rb +30 -0
- data/lib/harbor/application.rb +172 -0
- data/lib/harbor/auth/basic.rb +51 -0
- data/lib/harbor/block_io.rb +63 -0
- data/lib/harbor/cache.rb +90 -0
- data/lib/harbor/cache/disk.rb +99 -0
- data/lib/harbor/cache/item.rb +48 -0
- data/lib/harbor/cache/memory.rb +35 -0
- data/lib/harbor/cascade.rb +75 -0
- data/lib/harbor/console.rb +34 -0
- data/lib/harbor/container.rb +134 -0
- data/lib/harbor/contrib/debug.rb +236 -0
- data/lib/harbor/contrib/session/data_mapper.rb +74 -0
- data/lib/harbor/daemon.rb +105 -0
- data/lib/harbor/errors.rb +49 -0
- data/lib/harbor/events.rb +45 -0
- data/lib/harbor/exception_notifier.rb +59 -0
- data/lib/harbor/file.rb +66 -0
- data/lib/harbor/file_store.rb +69 -0
- data/lib/harbor/file_store/file.rb +100 -0
- data/lib/harbor/file_store/local.rb +71 -0
- data/lib/harbor/file_store/mosso.rb +154 -0
- data/lib/harbor/file_store/mosso/private.rb +8 -0
- data/lib/harbor/generator.rb +56 -0
- data/lib/harbor/generator/help.rb +34 -0
- data/lib/harbor/generator/setup.rb +82 -0
- data/lib/harbor/generator/skeletons/basic/config.ru.skel +21 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname.rb.skel +49 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/controllers/home.rb +9 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/home/index.html.erb.skel +23 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/application.html.erb.skel +48 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/exception.html.erb.skel +13 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/login.html.erb.skel +11 -0
- data/lib/harbor/generator/skeletons/basic/log/development.log +0 -0
- data/lib/harbor/hooks.rb +105 -0
- data/lib/harbor/json_cookies.rb +37 -0
- data/lib/harbor/layouts.rb +61 -0
- data/lib/harbor/locale.rb +50 -0
- data/lib/harbor/locales.txt +22 -0
- data/lib/harbor/logging.rb +39 -0
- data/lib/harbor/logging/appenders/email.rb +84 -0
- data/lib/harbor/logging/request_logger.rb +34 -0
- data/lib/harbor/mail_servers/abstract.rb +12 -0
- data/lib/harbor/mail_servers/sendmail.rb +19 -0
- data/lib/harbor/mail_servers/smtp.rb +25 -0
- data/lib/harbor/mail_servers/test.rb +17 -0
- data/lib/harbor/mailer.rb +96 -0
- data/lib/harbor/messages.rb +17 -0
- data/lib/harbor/mime.rb +206 -0
- data/lib/harbor/plugin.rb +52 -0
- data/lib/harbor/plugin_list.rb +38 -0
- data/lib/harbor/request.rb +138 -0
- data/lib/harbor/response.rb +281 -0
- data/lib/harbor/router.rb +112 -0
- data/lib/harbor/script.rb +155 -0
- data/lib/harbor/session.rb +75 -0
- data/lib/harbor/session/abstract.rb +27 -0
- data/lib/harbor/session/cookie.rb +17 -0
- data/lib/harbor/shellwords.rb +26 -0
- data/lib/harbor/test/mailer.rb +10 -0
- data/lib/harbor/test/request.rb +28 -0
- data/lib/harbor/test/response.rb +17 -0
- data/lib/harbor/test/session.rb +11 -0
- data/lib/harbor/test/test.rb +22 -0
- data/lib/harbor/version.rb +3 -0
- data/lib/harbor/view.rb +89 -0
- data/lib/harbor/view_context.rb +134 -0
- data/lib/harbor/view_context/helpers.rb +7 -0
- data/lib/harbor/view_context/helpers/cache.rb +77 -0
- data/lib/harbor/view_context/helpers/form.rb +34 -0
- data/lib/harbor/view_context/helpers/html.rb +26 -0
- data/lib/harbor/view_context/helpers/text.rb +120 -0
- data/lib/harbor/view_context/helpers/url.rb +11 -0
- data/lib/harbor/xml_view.rb +57 -0
- data/lib/harbor/zipped_io.rb +203 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles.rb +77 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/authentication.rb +46 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/connection.rb +280 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/container.rb +260 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/storage_object.rb +253 -0
- metadata +155 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Harbor
|
2
|
+
module Test
|
3
|
+
class Response < Harbor::Response
|
4
|
+
|
5
|
+
attr_accessor :request
|
6
|
+
|
7
|
+
##
|
8
|
+
# We redefine Harbor::Response.initialize(request) with an empty arg
|
9
|
+
# variant for use with a container.
|
10
|
+
##
|
11
|
+
def initialize
|
12
|
+
super(nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Harbor
|
2
|
+
module Test
|
3
|
+
|
4
|
+
require Pathname(__FILE__).dirname + "request"
|
5
|
+
require Pathname(__FILE__).dirname + "response"
|
6
|
+
require Pathname(__FILE__).dirname + "session"
|
7
|
+
require Pathname(__FILE__).dirname + "mailer"
|
8
|
+
|
9
|
+
def assert_redirect(response)
|
10
|
+
assert_equal 303, response.status, "Expected Response#status 303 but was #{response.status}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def assert_success(response)
|
14
|
+
assert_equal 200, response.status, "Expected Response#status 200 but was #{response.status}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def assert_unauthorized(response)
|
18
|
+
assert_equal 401, response.status, "Expected Response#status 401 but was #{response.status}"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
data/lib/harbor/view.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
gem "erubis"
|
4
|
+
require "erubis"
|
5
|
+
|
6
|
+
require Pathname(__FILE__).dirname + "view_context"
|
7
|
+
require Pathname(__FILE__).dirname + "layouts"
|
8
|
+
require Pathname(__FILE__).dirname + "plugin_list"
|
9
|
+
|
10
|
+
module Harbor
|
11
|
+
class View
|
12
|
+
|
13
|
+
class LayoutNotFoundError < StandardError
|
14
|
+
def initialize(name)
|
15
|
+
super("Layout #{name.inspect} not found")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.path
|
20
|
+
@path ||= []
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.layouts
|
24
|
+
@layouts ||= Harbor::Layouts.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.plugins(key)
|
28
|
+
@plugins ||= Hash.new { |h, k| h[k] = PluginList.new }
|
29
|
+
|
30
|
+
@plugins[key.to_s.gsub(/^\/+/, '')]
|
31
|
+
end
|
32
|
+
|
33
|
+
@cache_templates = false
|
34
|
+
def self.cache_templates?
|
35
|
+
@cache_templates
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.cache_templates!
|
39
|
+
@cache_templates = true
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.exists?(filename)
|
43
|
+
self.path.detect { |dir| ::File.file?(dir + filename) }
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_accessor :content_type, :context, :extension, :path
|
47
|
+
|
48
|
+
def initialize(view, context = {})
|
49
|
+
@content_type = "text/html"
|
50
|
+
@extension = ".html.erb"
|
51
|
+
@context = context.is_a?(ViewContext) ? context : ViewContext.new(self, context)
|
52
|
+
@filename = ::File.extname(view) == "" ? (view + @extension) : view
|
53
|
+
end
|
54
|
+
|
55
|
+
def supports_layouts?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
def content
|
60
|
+
@content ||= _erubis_render(@context)
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s(layout = nil)
|
64
|
+
layout = self.class.layouts.match(@filename) if layout == :search
|
65
|
+
|
66
|
+
layout ? View.new(layout, @context.merge(:content => content)).to_s : content
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def _erubis_render(context)
|
72
|
+
@path ||= self.class.exists?(@filename)
|
73
|
+
raise "Could not find '#{@filename}' in #{self.class.path.inspect}" unless @path
|
74
|
+
|
75
|
+
full_path = @path + @filename
|
76
|
+
|
77
|
+
if self.class.cache_templates?
|
78
|
+
(self.class.__templates[full_path] ||= Erubis::FastEruby.new(::File.read(full_path), :filename => full_path)).evaluate(context)
|
79
|
+
else
|
80
|
+
Erubis::FastEruby.new(::File.read(full_path), :filename => full_path).evaluate(context)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.__templates
|
85
|
+
@__templates ||= {}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Harbor
|
2
|
+
class ViewContext
|
3
|
+
require Pathname(__FILE__).dirname + "view_context/helpers"
|
4
|
+
|
5
|
+
include Helpers::Form
|
6
|
+
include Helpers::Text
|
7
|
+
include Helpers::Html
|
8
|
+
include Helpers::Url
|
9
|
+
include Helpers::Cache
|
10
|
+
|
11
|
+
attr_accessor :view, :keys
|
12
|
+
|
13
|
+
def initialize(view, variables)
|
14
|
+
@view = view
|
15
|
+
@keys = Set.new
|
16
|
+
|
17
|
+
merge(variables)
|
18
|
+
end
|
19
|
+
|
20
|
+
def render(partial, variables = nil)
|
21
|
+
context = to_hash
|
22
|
+
|
23
|
+
result = View.new(partial, merge(variables)).to_s
|
24
|
+
|
25
|
+
replace(context)
|
26
|
+
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def plugin(name, variables = {})
|
31
|
+
if (plugin_list = Harbor::View::plugins(name)).any?
|
32
|
+
plugin_list.map do |plugin|
|
33
|
+
Plugin::prepare(plugin, self, variables)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def locale
|
41
|
+
@locale ||= Harbor::Locale.default
|
42
|
+
end
|
43
|
+
|
44
|
+
def capture(*args, &block)
|
45
|
+
# get the buffer from the block's binding
|
46
|
+
buffer = _erb_buffer( block.binding ) rescue nil
|
47
|
+
|
48
|
+
# If there is no buffer, just call the block and get the contents
|
49
|
+
if buffer.nil?
|
50
|
+
block.call(*args)
|
51
|
+
# If there is a buffer, execute the block, then extract its contents
|
52
|
+
else
|
53
|
+
pos = buffer.length
|
54
|
+
|
55
|
+
block.call(*args)
|
56
|
+
|
57
|
+
# extract the block
|
58
|
+
data = buffer[pos..-1]
|
59
|
+
|
60
|
+
# replace it in the original with empty string
|
61
|
+
buffer[pos..-1] = ''
|
62
|
+
|
63
|
+
data
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](key)
|
68
|
+
instance_variable_get("@#{key}")
|
69
|
+
end
|
70
|
+
|
71
|
+
def []=(key, value)
|
72
|
+
@keys << key
|
73
|
+
instance_variable_set("@#{key}", value)
|
74
|
+
end
|
75
|
+
|
76
|
+
def merge(variables)
|
77
|
+
variables.each do |key, value|
|
78
|
+
self[key] = value
|
79
|
+
end if variables
|
80
|
+
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def each
|
85
|
+
keys.each { |key| yield(key, self[key]) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def clear
|
89
|
+
keys.each { |key| remove_instance_variable("@#{key}") }
|
90
|
+
keys.clear
|
91
|
+
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
def replace(variables)
|
96
|
+
clear
|
97
|
+
merge(variables)
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_hash
|
101
|
+
hash = {}
|
102
|
+
keys.each { |key| hash[key] = self[key] }
|
103
|
+
hash
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def request
|
109
|
+
@request
|
110
|
+
end
|
111
|
+
|
112
|
+
def response
|
113
|
+
@response
|
114
|
+
end
|
115
|
+
|
116
|
+
def _erb_buffer( the_binding ) # :nodoc:
|
117
|
+
eval( "_buf", the_binding, __FILE__, __LINE__)
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Useful when you need to output content to the buffer.
|
122
|
+
#
|
123
|
+
# def wrap_with_p_tag(&block)
|
124
|
+
# with_buffer(block) do |buffer|
|
125
|
+
# buffer << "<p>" << capture(&block) << "</p>"
|
126
|
+
# end
|
127
|
+
# end
|
128
|
+
##
|
129
|
+
def with_buffer(block)
|
130
|
+
yield(_erb_buffer(block.binding))
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
module Harbor::ViewContext::Helpers
|
2
|
+
autoload :Form, (Pathname(__FILE__).dirname + "helpers/form").to_s
|
3
|
+
autoload :Text, (Pathname(__FILE__).dirname + "helpers/text").to_s
|
4
|
+
autoload :Html, (Pathname(__FILE__).dirname + "helpers/html").to_s
|
5
|
+
autoload :Url, (Pathname(__FILE__).dirname + "helpers/url").to_s
|
6
|
+
autoload :Cache, (Pathname(__FILE__).dirname + "helpers/cache").to_s
|
7
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
##
|
2
|
+
# Set Harbor::View.cache equal to a supported Cache Store for use
|
3
|
+
# in the ViewContext#cache helper.
|
4
|
+
##
|
5
|
+
class Harbor::View
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def cache=(value)
|
10
|
+
if value && !value.is_a?(Harbor::Cache)
|
11
|
+
raise ArgumentError.new("Harbor::View.cache must be nil or an instance of Harbor::Cache")
|
12
|
+
end
|
13
|
+
|
14
|
+
@__cache__ = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def cache
|
18
|
+
@__cache__
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Cache helper that provides fragment-caching
|
27
|
+
##
|
28
|
+
module Harbor::ViewContext::Helpers::Cache
|
29
|
+
|
30
|
+
class CacheRenderError < StandardError
|
31
|
+
def initialize(inner_error, content_item)
|
32
|
+
@inner_error = inner_error
|
33
|
+
@content_item = content_item
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"#{@content_item.class.name}:#{@content_item.inspect}\n\t#{@inner_error.message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def inspect
|
41
|
+
"<#CacheRenderError content_item=#{@content_item.class.name}:#{@content_item.inspect} inner_error=#{@inner_error.inspect} backtrace=#{@inner_error.backtrace.join("\n\t")}>"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Caches the result of a block using the given TTL and maximum_age values.
|
47
|
+
# If no ttl is given, a default of 30 minutes is used. If no maximum_age value is given
|
48
|
+
# the item will expire after Time.now + ttl. If a maximum_age is specified, "get" requests
|
49
|
+
# to the cache for a given key will push the expiration time up for the item by the TTL, until
|
50
|
+
# Time.now + TTL is equal to or greater than the cache-insertion-time + maximum_age.
|
51
|
+
##
|
52
|
+
def cache(key, ttl = 30 * 60, max_age = nil, &generator)
|
53
|
+
store = @cache_store || Harbor::View.cache
|
54
|
+
|
55
|
+
if store.nil?
|
56
|
+
raise ArgumentError.new("Cache Store Not Defined. Please set Harbor::View.cache to your desired cache store.")
|
57
|
+
end
|
58
|
+
|
59
|
+
content = if item = store.get(key)
|
60
|
+
begin
|
61
|
+
item.content
|
62
|
+
rescue => e
|
63
|
+
raise CacheRenderError.new(e, item)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
data = capture(&generator)
|
67
|
+
store.put(key, data, ttl, max_age)
|
68
|
+
|
69
|
+
data
|
70
|
+
end
|
71
|
+
|
72
|
+
with_buffer(generator) do |buffer|
|
73
|
+
buffer << content
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Harbor::ViewContext::Helpers::Form
|
2
|
+
|
3
|
+
##
|
4
|
+
# Form helper which abstracts away setting necessary information:
|
5
|
+
# 1. If no enctype is passed, and a file input is found, uses
|
6
|
+
# multipart/form-data.
|
7
|
+
# 2. Creates a hidden input for setting _method when method option
|
8
|
+
# is put or delete.
|
9
|
+
##
|
10
|
+
def form(action, options = {}, &block)
|
11
|
+
method = options.delete(:method) || :post
|
12
|
+
|
13
|
+
get = method == :get
|
14
|
+
post = method == :post
|
15
|
+
|
16
|
+
body = capture(&block)
|
17
|
+
|
18
|
+
enctype = options.delete(:enctype) || (body =~ /\<input[^>]+?type\=["']file["']/ ? "multipart/form-data" : nil)
|
19
|
+
|
20
|
+
with_buffer(block) do |buffer|
|
21
|
+
buffer << "<form action=\"#{action}\" method=\"#{get ? "get" : "post"}\""
|
22
|
+
buffer << " enctype=\"#{enctype}\"" if enctype
|
23
|
+
buffer << " #{options.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")}" unless options.empty?
|
24
|
+
buffer << ">\n"
|
25
|
+
|
26
|
+
unless get || post
|
27
|
+
buffer << " <input type=\"hidden\" name=\"_method\" value=\"#{method}\">\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
buffer << body
|
31
|
+
buffer << "</form>\n"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Harbor::ViewContext::Helpers::Html
|
2
|
+
|
3
|
+
##
|
4
|
+
# Takes a flat array and yields the data properly separated into columns.
|
5
|
+
##
|
6
|
+
def split_into_columns(data, columns) #:yields: column
|
7
|
+
return if data.empty?
|
8
|
+
|
9
|
+
per_column = (data.size / columns.to_f).ceil
|
10
|
+
|
11
|
+
columns.times do |i|
|
12
|
+
yield data[i*per_column, per_column]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def split_into_groups(data, groups) #:yields: group
|
17
|
+
return if data.empty?
|
18
|
+
|
19
|
+
rows = (data.size / groups.to_f).ceil
|
20
|
+
|
21
|
+
rows.times do |i|
|
22
|
+
yield data[i*groups, groups]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
##
|
2
|
+
# Text helper which provides common routines such as HTML escaping,
|
3
|
+
# or truncating (previewing) long pieces of text (such as photo captions).
|
4
|
+
##
|
5
|
+
module Harbor::ViewContext::Helpers::Text
|
6
|
+
|
7
|
+
# Querystring escape +value+
|
8
|
+
def q(value)
|
9
|
+
# TODO: Remove external dependency!
|
10
|
+
Rack::Utils::escape(value)
|
11
|
+
end
|
12
|
+
|
13
|
+
# HTML escape +value+
|
14
|
+
def h(value, default = nil)
|
15
|
+
# TODO: Remove external dependency!
|
16
|
+
Rack::Utils::escape_html(value.blank? ? default : value)
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Truncates an object to the specified character count, appending the specified
|
21
|
+
# trailing text. The character count includes the length of the trailer. HTML entities
|
22
|
+
# are counted as 1 character in trailing.
|
23
|
+
#
|
24
|
+
# truncate("Lorem ipsum dolor sit amet, consectetur") # => "Lorem ipsum dolor sit amet, c…"
|
25
|
+
# truncate("Lorem ipsum dolor sit amet, consectetur", 20) # => "Lorem ipsum dolor s…"
|
26
|
+
# truncate("Lorem ipsum dolor sit amet, consectetur", 20, "...") # => "Lorem ipsum dolor..."
|
27
|
+
#
|
28
|
+
##
|
29
|
+
def truncate(value, character_count = 30, trailing = "…")
|
30
|
+
unless character_count.is_a?(Integer)
|
31
|
+
raise ArgumentError.new(
|
32
|
+
"Harbor::ViewContext::Helpers::Text#truncate[character_count] must be an Integer, was #{character_count.inspect}"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
unless character_count > 0
|
37
|
+
raise ArgumentError.new(
|
38
|
+
"Harbor::ViewContext::Helpers::Text#truncate[character_count] must be greater than zero, was #{character_count.inspect}."
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
unless trailing.is_a?(String)
|
43
|
+
raise ArgumentError.new(
|
44
|
+
"Harbor::ViewContext::Helpers::Text#truncate[trailing] must be a String, was #{trailing.inspect}"
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
if value.nil?
|
49
|
+
""
|
50
|
+
else
|
51
|
+
string_form = value.to_s
|
52
|
+
|
53
|
+
if string_form.nil? || string_form.empty?
|
54
|
+
""
|
55
|
+
elsif string_form.size <= character_count
|
56
|
+
string_form
|
57
|
+
else
|
58
|
+
# The Regexp match here is to determine if the +trailing+ value is an HTML entity code,
|
59
|
+
# in which case we assume it's length is 1, or a textual value, in which case we use the
|
60
|
+
# actual size.
|
61
|
+
string_form[0, character_count - (trailing =~ /\&\w+\;/ ? 1 : trailing.size)] + trailing
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Truncates an object on the nearest word to the specified character count, appending the specified
|
68
|
+
# trailing text.
|
69
|
+
#
|
70
|
+
# truncate_on_words("Lorem ipsum dolor sit amet, consectetur") # => "Lorem ipsum dolor sit amet…"
|
71
|
+
# truncate_on_words("Lorem ipsum dolor sit amet, consectetur", 20) # => "Lorem ipsum dolor…"
|
72
|
+
# truncate_on_words("Lorem ipsum dolor sit amet, consectetur", 20, "...") # => "Lorem ipsum dolor..."
|
73
|
+
#
|
74
|
+
# The truncation will always look backwards unless the forward word boundary is within 5% of the specified
|
75
|
+
# character count. Thus:
|
76
|
+
#
|
77
|
+
# truncate_on_words("Lorem ipsum dolor sit amet, consectetur est.", 38) # => "Lorem ipsum dolor sit amet, consectetur..."
|
78
|
+
#
|
79
|
+
##
|
80
|
+
def truncate_on_words(value, character_count = 30, trailing = "…")
|
81
|
+
unless character_count.is_a?(Integer)
|
82
|
+
raise ArgumentError.new(
|
83
|
+
"Harbor::ViewContext::Helpers::Text#truncate_on_words[character_count] must be an Integer, was #{character_count.inspect}"
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
unless character_count > 0
|
88
|
+
raise ArgumentError.new(
|
89
|
+
"Harbor::ViewContext::Helpers::Text#truncate_on_words[character_count] must be greater than zero, was #{character_count.inspect}."
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
unless trailing.is_a?(String)
|
94
|
+
raise ArgumentError.new(
|
95
|
+
"Harbor::ViewContext::Helpers::Text#truncate_on_words[trailing] must be a String, was #{trailing.inspect}"
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
return "" if value.nil?
|
100
|
+
|
101
|
+
truncated_text = value.to_s.dup
|
102
|
+
text_length = truncated_text.length
|
103
|
+
|
104
|
+
return value if character_count >= text_length
|
105
|
+
|
106
|
+
leftover = truncated_text.slice!(character_count, text_length)
|
107
|
+
|
108
|
+
if (index = leftover.index(/\W|$/)) && index < (character_count * 0.05).ceil
|
109
|
+
truncated_text << leftover.slice(0, index)
|
110
|
+
else
|
111
|
+
truncated_text = truncated_text[0, truncated_text.rindex(/\W/)]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Remove any trailing punctuation.
|
115
|
+
truncated_text.slice!(truncated_text.length - 1) if truncated_text[truncated_text.length - 1, 1] =~ /\W/
|
116
|
+
|
117
|
+
truncated_text + trailing
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|