PageRenderTime 27ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/text_helper.rb

https://github.com/lutvi-r/canvas-lms
Ruby | 333 lines | 254 code | 32 blank | 47 comment | 82 complexity | 13b59e3e63b43a76bf4d3906a9011e4a MD5 | raw file
  1. # By Henrik Nyh <http://henrik.nyh.se> 2008-01-30.
  2. # Free to modify and redistribute with credit.
  3. # modified by Dave Nolan <http://textgoeshere.org.uk> 2008-02-06
  4. # Ellipsis appended to text of last HTML node
  5. # Ellipsis inserted after final word break
  6. # modified by Mark Dickson <mark@sitesteaders.com> 2008-12-18
  7. # Option to truncate to last full word
  8. # Option to include a 'more' link
  9. # Check for nil last child
  10. # Copied from http://pastie.textmate.org/342485,
  11. # based on http://henrik.nyh.se/2008/01/rails-truncate-html-helper
  12. module TextHelper
  13. def strip_and_truncate(text, options={})
  14. truncate_text(strip_tags(text), options)
  15. end
  16. def strip_tags(text)
  17. text ||= ""
  18. text.gsub(/<\/?[^>\n]*>/, "").gsub(/&#\d+;/) {|m| puts m; m[2..-1].to_i.chr rescue '' }.gsub(/&\w+;/, "")
  19. end
  20. def quote_clump(quote_lines)
  21. txt = "<div class='quoted_text_holder'><a href='#' class='show_quoted_text_link'>show quoted text</a><div class='quoted_text' style='display: none;'>"
  22. txt += quote_lines.join("\n")
  23. txt += "</div></div>"
  24. txt
  25. end
  26. # http://daringfireball.net/2010/07/improved_regex_for_matching_urls
  27. # released to the public domain
  28. AUTO_LINKIFY_PLACEHOLDER = "LINK-PLACEHOLDER"
  29. AUTO_LINKIFY_REGEX = %r{
  30. \b
  31. ( # Capture 1: entire matched URL
  32. (?:
  33. https?:// # http or https protocol
  34. | # or
  35. www\d{0,3}[.] # "www.", "www1.", "www2." "www999."
  36. | # or
  37. [a-z0-9.\-]+[.][a-z]{2,4}/ # looks like domain name followed by a slash
  38. )
  39. (?: # One or more:
  40. [^\s()<>]+ # Run of non-space, non-()<>
  41. | # or
  42. \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
  43. )+
  44. (?: # End with:
  45. \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
  46. | # or
  47. [^\s`!()\[\]{};:'".,<>?«»“”‘’] # not a space or one of these punct chars
  48. )
  49. ) | (
  50. #{AUTO_LINKIFY_PLACEHOLDER}
  51. )
  52. }xi
  53. # Converts a plaintext message to html, with newlinification, quotification, and linkification
  54. def format_message(message, url=nil, notification_id=nil)
  55. # insert placeholders for the links we're going to generate, before we go and escape all the html
  56. links = []
  57. placeholder_blocks = []
  58. message = message.gsub(AUTO_LINKIFY_REGEX) do |match|
  59. placeholder_blocks << if match == AUTO_LINKIFY_PLACEHOLDER
  60. AUTO_LINKIFY_PLACEHOLDER
  61. else
  62. s = $1
  63. link = s
  64. link = "http://#{link}" if link[0,3] == 'www'
  65. link = add_notification_to_link(link, notification_id) if notification_id
  66. links << link
  67. "<a href='#{link}'>#{s}</a>"
  68. end
  69. AUTO_LINKIFY_PLACEHOLDER
  70. end
  71. # now escape any html
  72. message = TextHelper.escape_html(message)
  73. # now put the links back in
  74. message = message.gsub(AUTO_LINKIFY_PLACEHOLDER) do |match|
  75. placeholder_blocks.shift
  76. end
  77. message = message.gsub(/\r?\n/, "<br/>\r\n")
  78. processed_lines = []
  79. quote_block = []
  80. message.split("\n").each do |line|
  81. # check for lines starting with '>'
  82. if /^(&gt;|>)/ =~ line
  83. quote_block << line
  84. else
  85. processed_lines << quote_clump(quote_block) if !quote_block.empty?
  86. quote_block = []
  87. processed_lines << line
  88. end
  89. end
  90. processed_lines << quote_clump(quote_block) if !quote_block.empty?
  91. message = processed_lines.join("\n")
  92. if url
  93. url = add_notification_to_link(url, notification_id) if notification_id
  94. links.unshift url
  95. end
  96. links.unshift message
  97. end
  98. def add_notification_to_link(url, notification_id)
  99. parts = "#{url}".split("#", 2)
  100. link = parts[0]
  101. link += link.match(/\?/) ? "&" : "?"
  102. link += "clear_notification_id=#{notification_id}"
  103. link += parts[1] if parts[1]
  104. link
  105. rescue
  106. return ""
  107. end
  108. def truncate_text(text, options={})
  109. max_length = options[:max_length] || 30
  110. ellipsis = options[:ellipsis] || "..."
  111. ellipsis_length = ellipsis.length
  112. actual_length = max_length - ellipsis_length
  113. truncated = (text || "")[/.{0,#{actual_length}}/mu]
  114. if truncated.length < text.length
  115. truncated + ellipsis
  116. else
  117. truncated
  118. end
  119. end
  120. def self.escape_html(text)
  121. CGI::escapeHTML text
  122. end
  123. def self.unescape_html(text)
  124. CGI::unescapeHTML text
  125. end
  126. def hours_ago_in_words(from_time)
  127. diff = (Time.now - from_time).abs
  128. if diff < 60
  129. "< 1 minute"
  130. elsif diff < 3600
  131. "#{(diff / 60).to_i} minutes"
  132. else
  133. "#{(diff / 3600).to_i} hours"
  134. end
  135. end
  136. def indent(text, spaces=2)
  137. text = text.to_s rescue ""
  138. indentation = " " * spaces
  139. text.gsub(/\n/, "\n#{indentation}")
  140. end
  141. def force_zone(time)
  142. time_zone ||= @time_zone || Time.zone
  143. res = ActiveSupport::TimeWithZone.new(time.utc, time_zone) rescue nil
  144. res || time
  145. end
  146. def date_string(start_date, style=:normal)
  147. return nil unless start_date
  148. start_date = start_date.in_time_zone.to_date rescue start_date.to_date
  149. today = ActiveSupport::TimeWithZone.new(Time.now, Time.zone).to_date
  150. if style != :long
  151. return "Today" if style != :no_words && start_date == today
  152. return "Tomorrow" if style != :no_words && start_date == today + 1
  153. return "Yesterday" if style != :no_words && start_date == today - 1
  154. return start_date.strftime("%A") if style != :no_words && start_date < today + 1.week && start_date >= today
  155. return start_date.strftime("%b #{start_date.day}") if start_date.year == today.year || style == :short
  156. end
  157. return start_date.strftime("%b #{start_date.day}, %Y")
  158. end
  159. def time_string(start_time, end_time=nil)
  160. start_time = start_time.in_time_zone rescue start_time
  161. end_time = end_time.in_time_zone rescue end_time
  162. return nil unless start_time
  163. hr = start_time.hour % 12
  164. hr = 12 if hr == 0
  165. result = hr.to_s + (start_time.min == 0 ? start_time.strftime("%p").downcase : start_time.strftime(":%M%p").downcase)
  166. if end_time && end_time != start_time
  167. result = result + " to " + time_string(end_time)
  168. end
  169. result
  170. end
  171. def datetime_span(*args)
  172. string = datetime_string(*args)
  173. if string && !string.empty? && args[0]
  174. "<span class='zone_cached_datetime' title='#{args[0].iso8601 rescue ""}'>#{string}</span>"
  175. else
  176. nil
  177. end
  178. end
  179. def datetime_string(start_datetime, datetime_type=:event, end_datetime=nil, shorten_midnight=false)
  180. start_datetime = start_datetime.in_time_zone rescue start_datetime
  181. return nil unless start_datetime
  182. end_datetime = end_datetime.in_time_zone rescue end_datetime
  183. if !datetime_type.is_a?(Symbol)
  184. datetime_type = :event
  185. end_datetime = nil
  186. end
  187. start_time = time_string(start_datetime)
  188. by_at = datetime_type == :due_date ? ' by' : ' at'
  189. # I am assuming that by the time we get here that start_datetime will be in the same time zone as @current_user's timezone
  190. if shorten_midnight && start_datetime && ((datetime_type == :due_date && start_datetime.hour == 23 && start_datetime.min == 59) || (datetime_type == :event && start_datetime.hour == 0 && start_datetime.min == 0))
  191. start_time = ''
  192. by_at = ''
  193. end
  194. def datestring(datetime)
  195. return datetime.strftime("%b #{datetime.day}") if datetime.year == ActiveSupport::TimeWithZone.new(Time.now, Time.zone).to_date.year
  196. return datetime.strftime("%b #{datetime.day}, %Y")
  197. end
  198. unless(end_datetime && end_datetime != start_datetime)
  199. result = (datetime_type == :verbose ? start_datetime.strftime("%a, ") : "") + datestring(start_datetime) + by_at + " " + start_time
  200. else
  201. end_time = time_string(end_datetime)
  202. unless start_datetime.to_date != end_datetime.to_date
  203. result = datestring(start_datetime) + " from " + start_time + " to " + end_time
  204. else
  205. result = (datetime_type == :verbose ? start_datetime.strftime("%a, ") : "") + datestring(start_datetime) + by_at + " " + start_time + " to " + (datetime_type == :verbose ? end_datetime.strftime("%a, ") : "") + datestring(end_datetime) + by_at + " " + end_time
  206. end
  207. end
  208. result
  209. rescue
  210. nil
  211. end
  212. def truncate_html(input, options={})
  213. doc = Nokogiri::HTML(input)
  214. options[:max_length] ||= 250
  215. num_words = options[:num_words] || (options[:max_length] / 5) || 30
  216. truncate_string = options[:ellipsis] || "..."
  217. truncate_string += options[:link] if options[:link]
  218. truncate_elem = Nokogiri::HTML("<span>" + truncate_string + "</span>").at("span")
  219. current = doc.children.first
  220. count = 0
  221. while true
  222. # we found a text node
  223. if current.is_a?(Nokogiri::XML::Text)
  224. count += current.text.split.length
  225. # we reached our limit, let's get outta here!
  226. break if count > num_words
  227. previous = current
  228. end
  229. if current.children.length > 0
  230. # this node has children, can't be a text node,
  231. # lets descend and look for text nodes
  232. current = current.children.first
  233. elsif !current.next.nil?
  234. #this has no children, but has a sibling, let's check it out
  235. current = current.next
  236. else
  237. # we are the last child, we need to ascend until we are
  238. # either done or find a sibling to continue on to
  239. n = current
  240. while !n.is_a?(Nokogiri::HTML::Document) and n.parent.next.nil?
  241. n = n.parent
  242. end
  243. # we've reached the top and found no more text nodes, break
  244. if n.is_a?(Nokogiri::HTML::Document)
  245. break;
  246. else
  247. current = n.parent.next
  248. end
  249. end
  250. end
  251. if count >= num_words
  252. unless count == num_words
  253. new_content = current.text.split
  254. # If we're here, the last text node we counted eclipsed the number of words
  255. # that we want, so we need to cut down on words. The easiest way to think about
  256. # this is that without this node we'd have fewer words than the limit, so all
  257. # the previous words plus a limited number of words from this node are needed.
  258. # We simply need to figure out how many words are needed and grab that many.
  259. # Then we need to -subtract- an index, because the first word would be index zero.
  260. # For example, given:
  261. # <p>Testing this HTML truncater.</p><p>To see if its working.</p>
  262. # Let's say I want 6 words. The correct returned string would be:
  263. # <p>Testing this HTML truncater.</p><p>To see...</p>
  264. # All the words in both paragraphs = 9
  265. # The last paragraph is the one that breaks the limit. How many words would we
  266. # have without it? 4. But we want up to 6, so we might as well get that many.
  267. # 6 - 4 = 2, so we get 2 words from this node, but words #1-2 are indices #0-1, so
  268. # we subtract 1. If this gives us -1, we want nothing from this node. So go back to
  269. # the previous node instead.
  270. index = num_words-(count-new_content.length)-1
  271. if index >= 0
  272. new_content = new_content[0..index]
  273. current.add_previous_sibling(truncate_elem)
  274. new_node = Nokogiri::XML::Text.new(new_content.join(' '), doc)
  275. truncate_elem.add_previous_sibling(new_node)
  276. current = current.previous
  277. else
  278. current = previous
  279. # why would we do this next line? it just ends up xml escaping stuff
  280. #current.content = current.content
  281. current.add_next_sibling(truncate_elem)
  282. current = current.next
  283. end
  284. end
  285. # remove everything else
  286. while !current.is_a?(Nokogiri::HTML::Document)
  287. while !current.next.nil?
  288. current.next.remove
  289. end
  290. current = current.parent
  291. end
  292. end
  293. # now we grab the html and not the text.
  294. # we do first because nokogiri adds html and body tags
  295. # which we don't want
  296. res = doc.at_css('body').inner_html rescue nil
  297. res ||= doc.root.children.first.inner_html rescue ""
  298. res && res.html_safe
  299. end
  300. end