2011-09-27 12:50:30 +08:00
|
|
|
# encoding: UTF-8
|
|
|
|
#
|
2011-02-01 09:57:29 +08:00
|
|
|
# By Henrik Nyh <http://henrik.nyh.se> 2008-01-30.
|
|
|
|
# Free to modify and redistribute with credit.
|
|
|
|
|
|
|
|
# modified by Dave Nolan <http://textgoeshere.org.uk> 2008-02-06
|
|
|
|
# Ellipsis appended to text of last HTML node
|
|
|
|
# Ellipsis inserted after final word break
|
|
|
|
|
|
|
|
# modified by Mark Dickson <mark@sitesteaders.com> 2008-12-18
|
|
|
|
# Option to truncate to last full word
|
|
|
|
# Option to include a 'more' link
|
|
|
|
# Check for nil last child
|
|
|
|
|
2012-01-21 01:47:43 +08:00
|
|
|
# Copied from http://pastie.textmate.org/342485,
|
2011-02-01 09:57:29 +08:00
|
|
|
# based on http://henrik.nyh.se/2008/01/rails-truncate-html-helper
|
|
|
|
|
2015-04-09 01:21:08 +08:00
|
|
|
require 'nokogiri'
|
|
|
|
require 'rdiscount'
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
module TextHelper
|
|
|
|
def force_zone(time)
|
2011-06-09 08:39:01 +08:00
|
|
|
(time.in_time_zone(@time_zone || Time.zone) rescue nil) || time
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
|
2012-01-07 07:58:18 +08:00
|
|
|
def self.date_string(start_date, *args)
|
2011-02-01 09:57:29 +08:00
|
|
|
return nil unless start_date
|
2015-05-05 07:32:01 +08:00
|
|
|
start_date = start_date.in_time_zone.beginning_of_day
|
2012-01-07 07:58:18 +08:00
|
|
|
style = args.last.is_a?(Symbol) ? args.pop : :normal
|
|
|
|
end_date = args.pop
|
2015-05-05 07:32:01 +08:00
|
|
|
end_date = end_date.in_time_zone.beginning_of_day if end_date
|
2014-10-01 07:51:02 +08:00
|
|
|
start_date_display = Utils::DatePresenter.new(start_date).as_string(style)
|
2012-01-07 07:58:18 +08:00
|
|
|
if end_date.nil? || start_date == end_date
|
2014-10-01 07:51:02 +08:00
|
|
|
start_date_display
|
2012-01-07 07:58:18 +08:00
|
|
|
else
|
|
|
|
I18n.t('time.ranges.different_days', "%{start_date_and_time} to %{end_date_and_time}",
|
2014-10-01 07:51:02 +08:00
|
|
|
:start_date_and_time => start_date_display,
|
|
|
|
:end_date_and_time => Utils::DatePresenter.new(end_date).as_string(style)
|
2012-01-07 07:58:18 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def date_string(*args)
|
|
|
|
TextHelper.date_string(*args)
|
2011-10-06 01:56:38 +08:00
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
|
2014-05-14 07:48:56 +08:00
|
|
|
def time_string(start_time, end_time=nil, zone=nil)
|
2014-10-01 07:51:02 +08:00
|
|
|
presenter = Utils::TimePresenter.new(start_time, zone)
|
|
|
|
presenter.as_string(display_as_range: end_time)
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def datetime_span(*args)
|
|
|
|
string = datetime_string(*args)
|
|
|
|
if string && !string.empty? && args[0]
|
|
|
|
"<span class='zone_cached_datetime' title='#{args[0].iso8601 rescue ""}'>#{string}</span>"
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-11-10 15:07:13 +08:00
|
|
|
def datetime_string(start_datetime, datetime_type=:event, end_datetime=nil, shorten_midnight=false, zone=nil)
|
|
|
|
zone ||= ::Time.zone
|
2014-10-01 07:51:02 +08:00
|
|
|
presenter = Utils::DatetimeRangePresenter.new(start_datetime, end_datetime, datetime_type, zone)
|
|
|
|
presenter.as_string(shorten_midnight: shorten_midnight)
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
|
2011-06-10 11:51:23 +08:00
|
|
|
def time_ago_in_words_with_ago(time)
|
|
|
|
I18n.t('#time.with_ago', '%{time} ago', :time => (time_ago_in_words time rescue ''))
|
|
|
|
end
|
|
|
|
|
2011-06-17 06:12:13 +08:00
|
|
|
# more precise than distance_of_time_in_words, and takes a number of seconds,
|
|
|
|
# rather than two times. also assumes durations on the scale of hours or
|
|
|
|
# less, so doesn't bother with days, months, or years
|
|
|
|
def readable_duration(seconds)
|
|
|
|
# keys stolen from ActionView::Helpers::DateHelper#distance_of_time_in_words
|
|
|
|
case seconds
|
|
|
|
when 0...60
|
|
|
|
I18n.t('datetime.distance_in_words.x_seconds',
|
|
|
|
{ :one => "1 second", :other => "%{count} seconds" },
|
|
|
|
:count => seconds.round)
|
|
|
|
when 60...3600
|
|
|
|
I18n.t('datetime.distance_in_words.x_minutes',
|
|
|
|
{ :one => "1 minute", :other => "%{count} minutes" },
|
|
|
|
:count => (seconds / 60.0).round)
|
|
|
|
else
|
|
|
|
I18n.t('datetime.distance_in_words.about_x_hours',
|
|
|
|
{ :one => "about 1 hour", :other => "about %{count} hours" },
|
|
|
|
:count => (seconds / 3600.0).round)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
def truncate_html(input, options={})
|
|
|
|
doc = Nokogiri::HTML(input)
|
|
|
|
options[:max_length] ||= 250
|
|
|
|
num_words = options[:num_words] || (options[:max_length] / 5) || 30
|
2011-05-07 02:44:34 +08:00
|
|
|
truncate_string = options[:ellipsis] || I18n.t('lib.text_helper.ellipsis', '...')
|
2011-02-01 09:57:29 +08:00
|
|
|
truncate_string += options[:link] if options[:link]
|
|
|
|
truncate_elem = Nokogiri::HTML("<span>" + truncate_string + "</span>").at("span")
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
current = doc.children.first
|
|
|
|
count = 0
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
while true
|
|
|
|
# we found a text node
|
|
|
|
if current.is_a?(Nokogiri::XML::Text)
|
|
|
|
count += current.text.split.length
|
|
|
|
# we reached our limit, let's get outta here!
|
|
|
|
break if count > num_words
|
|
|
|
previous = current
|
|
|
|
end
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
if current.children.length > 0
|
|
|
|
# this node has children, can't be a text node,
|
|
|
|
# lets descend and look for text nodes
|
|
|
|
current = current.children.first
|
|
|
|
elsif !current.next.nil?
|
|
|
|
#this has no children, but has a sibling, let's check it out
|
|
|
|
current = current.next
|
2012-01-21 01:47:43 +08:00
|
|
|
else
|
2011-02-01 09:57:29 +08:00
|
|
|
# we are the last child, we need to ascend until we are
|
|
|
|
# either done or find a sibling to continue on to
|
|
|
|
n = current
|
|
|
|
while !n.is_a?(Nokogiri::HTML::Document) and n.parent.next.nil?
|
|
|
|
n = n.parent
|
|
|
|
end
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
# we've reached the top and found no more text nodes, break
|
|
|
|
if n.is_a?(Nokogiri::HTML::Document)
|
|
|
|
break;
|
|
|
|
else
|
|
|
|
current = n.parent.next
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
if count >= num_words
|
|
|
|
unless count == num_words
|
|
|
|
new_content = current.text.split
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
# If we're here, the last text node we counted eclipsed the number of words
|
|
|
|
# that we want, so we need to cut down on words. The easiest way to think about
|
|
|
|
# this is that without this node we'd have fewer words than the limit, so all
|
|
|
|
# the previous words plus a limited number of words from this node are needed.
|
|
|
|
# We simply need to figure out how many words are needed and grab that many.
|
|
|
|
# Then we need to -subtract- an index, because the first word would be index zero.
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
# For example, given:
|
|
|
|
# <p>Testing this HTML truncater.</p><p>To see if its working.</p>
|
|
|
|
# Let's say I want 6 words. The correct returned string would be:
|
|
|
|
# <p>Testing this HTML truncater.</p><p>To see...</p>
|
|
|
|
# All the words in both paragraphs = 9
|
|
|
|
# The last paragraph is the one that breaks the limit. How many words would we
|
|
|
|
# have without it? 4. But we want up to 6, so we might as well get that many.
|
|
|
|
# 6 - 4 = 2, so we get 2 words from this node, but words #1-2 are indices #0-1, so
|
|
|
|
# we subtract 1. If this gives us -1, we want nothing from this node. So go back to
|
|
|
|
# the previous node instead.
|
|
|
|
index = num_words-(count-new_content.length)-1
|
|
|
|
if index >= 0
|
|
|
|
new_content = new_content[0..index]
|
|
|
|
current.add_previous_sibling(truncate_elem)
|
|
|
|
new_node = Nokogiri::XML::Text.new(new_content.join(' '), doc)
|
|
|
|
truncate_elem.add_previous_sibling(new_node)
|
2012-01-11 11:57:19 +08:00
|
|
|
current = truncate_elem
|
2011-02-01 09:57:29 +08:00
|
|
|
else
|
|
|
|
current = previous
|
|
|
|
# why would we do this next line? it just ends up xml escaping stuff
|
|
|
|
#current.content = current.content
|
|
|
|
current.add_next_sibling(truncate_elem)
|
2012-01-11 11:57:19 +08:00
|
|
|
current = truncate_elem
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
end
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
# remove everything else
|
|
|
|
while !current.is_a?(Nokogiri::HTML::Document)
|
|
|
|
while !current.next.nil?
|
|
|
|
current.next.remove
|
|
|
|
end
|
|
|
|
current = current.parent
|
|
|
|
end
|
|
|
|
end
|
2012-01-21 01:47:43 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
# now we grab the html and not the text.
|
|
|
|
# we do first because nokogiri adds html and body tags
|
|
|
|
# which we don't want
|
|
|
|
res = doc.at_css('body').inner_html rescue nil
|
|
|
|
res ||= doc.root.children.first.inner_html rescue ""
|
2011-02-01 12:54:52 +08:00
|
|
|
res && res.html_safe
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2011-05-07 02:44:34 +08:00
|
|
|
|
2011-06-09 06:34:15 +08:00
|
|
|
def self.make_subject_reply_to(subject)
|
|
|
|
blank_re = I18n.t('#subject_reply_to', "Re: %{subject}", :subject => '')
|
|
|
|
return subject if subject.starts_with?(blank_re)
|
|
|
|
I18n.t('#subject_reply_to', "Re: %{subject}", :subject => subject)
|
|
|
|
end
|
|
|
|
|
2011-05-07 02:44:34 +08:00
|
|
|
class MarkdownSafeBuffer < String; end
|
|
|
|
# use this to flag interpolated parameters as markdown-safe (see
|
|
|
|
# mt below) so they get eval'ed rather than escaped, e.g.
|
|
|
|
# mt(:add_description, :example => markdown_safe('`1 + 1 = 2`'))
|
|
|
|
def markdown_safe(string)
|
|
|
|
MarkdownSafeBuffer.new(string)
|
|
|
|
end
|
|
|
|
|
|
|
|
def markdown_escape(string)
|
|
|
|
return string if string.is_a?(MarkdownSafeBuffer)
|
|
|
|
markdown_safe(string.gsub(/([\\`\*_\{\}\[\]\(\)\#\+\-\.!])/, '\\\\\1'))
|
|
|
|
end
|
|
|
|
|
|
|
|
# use this rather than t() if the translation contains trusted markdown
|
|
|
|
def mt(*args)
|
|
|
|
inlinify = :auto
|
|
|
|
if args.last.is_a?(Hash)
|
|
|
|
options = args.last
|
|
|
|
inlinify = options.delete(:inlinify) if options.has_key?(:inlinify)
|
|
|
|
options.each_pair do |key, value|
|
2011-06-17 03:55:59 +08:00
|
|
|
next unless value.is_a?(String) && !value.is_a?(MarkdownSafeBuffer) && !value.is_a?(ActiveSupport::SafeBuffer)
|
2011-06-16 06:46:20 +08:00
|
|
|
next if key == :wrapper
|
2011-05-07 02:44:34 +08:00
|
|
|
options[key] = markdown_escape(value).gsub(/\s+/, ' ').strip
|
|
|
|
end
|
|
|
|
end
|
2011-06-16 06:46:20 +08:00
|
|
|
translated = t(*args)
|
2012-12-07 14:28:37 +08:00
|
|
|
markdown(translated, inlinify)
|
|
|
|
end
|
|
|
|
|
|
|
|
def markdown(string, inlinify = :auto)
|
|
|
|
string = ERB::Util.h(string) unless string.html_safe?
|
|
|
|
result = RDiscount.new(string).to_html.strip
|
2011-06-22 01:29:04 +08:00
|
|
|
# Strip wrapping <p></p> if inlinify == :auto && they completely wrap the result && there are not multiple <p>'s
|
|
|
|
result.gsub!(/<\/?p>/, '') if inlinify == :auto && result =~ /\A<p>.*<\/p>\z/m && !(result =~ /.*<p>.*<p>.*/m)
|
2013-12-17 04:35:20 +08:00
|
|
|
result.strip.html_safe
|
2011-05-07 02:44:34 +08:00
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|