# frozen_string_literal: true
class ReeText::Excerpt
include Ree::FnDSL
fn :excerpt do
link :is_blank, from: :ree_object
end
DEFAULTS = {
radius: 100,
omission: "...",
separator: ""
}
doc(<<~DOC)
Extracts an excerpt from +text+ that matches the first instance of +phrase+.
The :radius option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
defined in :radius (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
then the :omission option (which defaults to "...") will be prepended/appended accordingly. Use the
:separator option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+
isn't found, +nil+ is returned.
excerpt('This is an example', 'an', radius: 5)
# => ...s is an exam...
excerpt('This is an example', 'is', radius: 5)
# => This is a...
excerpt('This is an example', 'is')
# => This is an example
excerpt('This next thing is an example', 'ex', radius: 2)
# => ...next...
excerpt('This is also an example', 'an', radius: 8, omission: ' ')
# => is also an example
excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1)
# => ...a very beautiful...
DOC
contract(
String,
Or[String, Regexp],
Ksplat[
radius?: Integer,
omission?: String,
separator?: String
] => String
)
def call(text, phrase, **opts)
options = DEFAULTS.merge(opts)
return if is_blank(text) && is_blank(phrase)
separator = options[:separator]
case phrase
when Regexp
regex = phrase
else
regex = /#{Regexp.escape(phrase)}/i
end
return unless matches = text.match(regex)
phrase = matches[0]
unless is_blank(separator)
text.split(separator).each do |value|
if value.match?(regex)
phrase = value
break
end
end
end
first_part, second_part = text.split(phrase, 2)
prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
affix = [first_part, separator, phrase, separator, second_part].join.strip
[prefix, affix, postfix].join
end
private
def cut_excerpt_part(part_position, part, separator, options)
return "", "" unless part
radius = options[:radius]
omission = options[:omission]
if separator != ""
part = part.split(separator)
part.delete("")
end
affix = part.length > radius ? omission : ""
part = if part_position == :first
last(part, radius)
else
first(part, radius)
end
if separator != ""
part = part.join(separator)
end
return affix, part
end
def first(string, limit = 1)
string[0, limit] || raise(ArgumentError, "negative limit")
end
def last(string, limit = 1)
string[[string.length - limit, 0].max, limit] || raise(ArgumentError, "negative limit")
end
end