Rails: using gsub to find and make links - ruby-on-rails

I've got a text area called body in a model. I'd like to change every image to a link to that image. I've got this method which is supposed to do that but it doesn't seem to work. It breaks at each image.
def get_images_in(body)
body_with_links = body.gsub( %r{http://[^\s<]+} ) do |url|
if url[/(?:png|jpe?g|gif|svg)$/]
"<a href='#{url}' target='_blank'><img src='#{url}' /></a>"
end
end
return body_with_links
end
Any ideas? Thank-you!
UPDATE
Here's a link to a gist with sample body text.

First things first, you don't need to use a return statement in ruby. Ruby will return the last thing by default. In your case, this is the string returned from the gsub:
def wrap_image_with_link(body)
body.gsub( %r{http://[^\s<]+} ) do |url|
if url[/(?:png|jpe?g|gif|svg)$/]
"<a href='#{url}' target='_blank'><img src='#{url}' /></a>"
end
end
end
This still isn't quite right. I would then focus my initial regular expression on the img tag:
def wrap_image_with_link(body)
body.gsub(/\<img.*src\=['"](.*)['"].*\/\>/) do |url|
"<a href='#{url}' target='_blank'><img src='#{url}' /></a>"
end
end
Rails has a couple helpers to clean this up.
def wrap_images_with_links(body)
body.gsub(/\<img.*src\=['"](.*)['"].*\/\>/) do
link_to(image_tag($1), $1, :target => '_blank')
end
end
You probably want to make the reg ex case insensitive:
def wrap_images_with_links(body)
body.gsub(/.*\<img.*src\=['"](.*)['"].*\/\>/i) do
link_to(image_tag($1), $1, :target => '_blank')
end
end
So gsub, as opposed to sub, changes every instance of matched reg-ex, so need to tweak the reg ex slightly to accommodate more explicit matches:
def wrap_images_with_links(body)
body.gsub(/\<img[\s\w"=]+src\=['"](http[s]?:\/\/[\w.\/]+)['"]\s?[\/]?\>/i) do
link_to(image_tag($1), $1, :target => '_blank')
end
end
Depending on the complexities of your urls, you might need to tweak the reg-ex to support ? or characters other than white space, but this work pretty well.

Related

RSpec "change all" matcher

I've got some job that updates records, and I want something like:
it 'updates each record' do
expect {
described_class.perform_now
}.to(change_all_of{
Record.pluck(:updated_at)
})
end
Only I can't find anything that looks like how to accomplish this, or what I can recognize as the docs for how to write a custom matcher.
The issue is that change, on an array, will return true if any element has changed; I only want to return true if every element has changed.
Can anyone point me either at whatever I missed that would let me do this, OR, whatever docs/info I need to write my own change matcher?
Alright, thanks to this answer on another question, and staring at the actual code, here's what I've got -
module RSpec
module Matchers
def change_all &block
BuiltIn::ChangeAll.new(nil, nil, &block)
end
module BuiltIn
class ChangeAll < Change
def initialize receiver=nil, message=nil, &block
#change_details = ChangeAllDetails.new(receiver, message, &block)
end
def failure_message
"expected all elements to change, but " + (
#change_details.actual_after & #change_details.actual_before
).collect do |unchanged|
"before[#{#change_details.actual_before.find_index(unchanged)}] " \
"after[#{#change_details.actual_after.find_index(unchanged)}] " \
"#{unchanged}"
end.join(",") + " remained the same"
end
end
class ChangeAllDetails < ChangeDetails
attr_accessor :actual_before
def changed?
!(#actual_after & #actual_before).any?
end
end
end
end
end
Suggestions welcome!

Ruby: strip iframe and convert its src to a var

I'm trying to parse a string that has an iframe in it, convert its src attribute to a specially-formatted Ruby variable, then replace the iframe in the string with the Ruby variable formatted in a particular way. So far I've written this:
def video_parse(string)
if string.include?('youtube.com/?v=')
url = 'youtube.com/?v='
string.gsub!('<iframe>.*</iframe>', video_service('YOUTUBE', vid(string, url)))
end
if string.include?('player.vimeo.com/video/')
url = 'player.vimeo.com/video/'
string.gsub!('<iframe>.*</iframe>', video_service('VIMEO', vid(string, url)))
end
string
end
def vid(string, url)
string.split(url).last.split(/['"]/).first
end
def video_service(service, vid)
"*|#{service}:[$vid=#{vid}]|*"
end
But it doesn't replace anything. I suspect my wildcard iframe tag selection is wrong, plus my vid method is a little clunky. How can I get my wildcard in gsub to work correctly? And for bonus points, can I write it a little more efficiently so I'm not parsing string to reformat the src in iframe?
Update
String looks something like this:
string = 'resources rather than creating our luck through innovation.\n<br>\n<br> \n<iframe allowfullscreen=\"\" frameborder=\"0\" height=\"311\" mozallowfullscreen=\"\" name=\"vimeo\" src=\"http://player.vimeo.com/video/222234444\" webkitallowfullscreen=\"\" width=\"550\"></iframe>\n<br>\n<br>\nThat hasn’t stoppe'
Second attempt looks like this, still doesn't replace anything:
def mailchimp_video_parse(string)
if string.include?('youtube.com/?v=')
string.gsub!(iframe) { video_service('YOUTUBE', vid(Regexp.last_match[1])) }
end
if string.include?('player.vimeo.com/video/')
string.gsub!(iframe) { video_service('VIMEO', vid(Regexp.last_match[1])) }
end
string
end
def vid(iframe)
iframe.split!('src').last.split!(/"/).first
end
def iframe
'<iframe.*<\/iframe>'
end
def video_service(service, vid)
"*|#{service}:[$vid=#{vid}]|*"
end
Still nothing.
A bit safer with Nokogiri:
d = Nokogiri::HTML(string)
d.css('iframe').each do |i|
if i['src'] =~ %r{(youtube|vimeo).*?([^/]+)$}i
i.replace(video_service($1.upcase, $2)
end
end
puts d.to_html
(But note that it is less efficient than the pure regexp solution, as Nokogiri will parse the whole HTML.)
The iframe method should be /<iframe.*<\/iframe>/ for it to be properly be recognized as a regex
The Regexp.last_match[1] should be Regexp.last_match[0] in the mailchimp_video_parse method
split! needs to be just split in the vid method (there is no split! method in Ruby)
Edited methods:
def mailchimp_video_parse(string)
if string.include?('youtube.com/?v=')
string.gsub!(iframe) { video_service('YOUTUBE', vid(Regexp.last_match[0])) }
end
if string.include?('player.vimeo.com/video/')
string.gsub!(iframe) { video_service('VIMEO', vid(Regexp.last_match[0])) }
end
string
end
def vid(iframe)
iframe.split('src').last.split(/"/).first
end
def iframe
/<iframe.*<\/iframe>/
end

How to make this code more Ruby-way?

I am new to Ruby and Rails (switched from Python and Python frameworks). I'm writing a simple dashboard website which displays information about the S.M.A.R.T. state of hard disks. Here I wrote a helper to display a badge in a table cell near the relevant S.M.A.R.T attribute if it's value meets a condition. At first, the helper code was as simple as in Listing 1, but then I decided to draw a summary of all badges for the specific drive, in addition to the badges near individual S.M.A.R.T. attributes in the table. So at first I added a simple method like:
def smart_chk_device(attrs)
attrs.each { |item| smart_chk_attr(item) }
end
But this approach didn't work and it caused the entire array of attributes to be output to the resulting page. It only started to work when I made it as in Listing 2, but I believe there's something wrong there, and the same thing can be done in a more simple way. Please show me the right, Ruby-way of doing it.
Listing 1:
module HomeHelper
def smart_chk_attr(attr)
case attr[:id].to_i
when 1,197
content_tag(:span, "Warning", :class => "label label-warning") if attr[:raw].to_i > 0
when 5,7,10,196,198
content_tag(:span, "Critical", :class => "label label-important") if attr[:raw].to_i > 0
end
end
end
Listing 2 (works, but I don't like it):
module HomeHelper
def smart_chk_attr(attr)
case attr[:id].to_i
when 1,197
return content_tag(:span, "Warning", :class => "label label-warning") if attr[:raw].to_i > 0
when 5,7,10,196,198
return content_tag(:span, "Critical", :class => "label label-important") if attr[:raw].to_i > 0
else
return String.new
end
return String.new
end
def smart_chk_device(attrs)
output = ""
attrs.each { |item| output << smart_chk_attr(item) }
return output.html_safe
end
end
attrs is an Array of Hashes, where each Hash contains keys :id and :raw with the numeric code of a S.M.A.R.T attribute and its RAW value, both in Strings.
Also, RoR complaints if to remove the last "return String.new" in Listing 2. Why is it so? Doesn't the "case" block all the possible cases, so that control should never reach the end of the function?
I believe this would behave in the same way, and is much shorter:
module HomeHelper
def smart_chk_attr(attr)
return '' unless attr[:raw].to_i > 0
case attr[:id].to_i
when 1,197
content_tag(:span, "Warning", :class => "label label-warning")
when 5,7,10,196,198
content_tag(:span, "Critical", :class => "label label-important")
else ''
end
end
def smart_chk_device(attrs)
attrs.map { |item| smart_chk_attr(item) }.join.html_safe
end
end
Ruby methods return the value of the last expression, so get rid of the explicit returns all over that method. Also, DRY: Don't Repeat Yourself (the attr[:raw] check). In this case, I replaced those with a guard clause at the start of the method. Short-circuiting guard clauses are a matter of taste, but I like them and you'll see them in a lot of Ruby code.
Your method smart_chk_attr(attr) has an extra return at the end, it will never get run.
The each is an enumerator, when it finishes going through each item that you supplied it returns the the original object passed into it, not the modified stuff inside.
If you use collect you will get an array with your modified objects.
If you are wanting a string output you can use join to put them into a string. Join will also take an option for how to join your items.
def smart_chk_device(attrs)
attrs.collect{ |item| smart_chk_attr(item) }.join.html_safe
end

Rails sanitize remove default allowed tags

How would I use sanitize, but tell it to disallow some enabled by default tags? The documentation states that I can put this in my application.rb
config.after_initialize do
ActionView::Base.sanitized_allowed_tags.delete 'div'
end
Can I instead pass this as an argument to sanitize?
Yes you can specify which tags and attributes to allow on a per-call basis. From the fine manual:
Custom Use (only the mentioned tags and attributes are allowed, nothing else)
<%= sanitize #article.body, :tags => %w(table tr td), :attributes => %w(id class style) %>
But the problem with that is that :tags has to include all the tags you want to allow.
The sanitize documentation says to
See ActionView::Base for full docs on the available options.
but the documentation is a lie, ActionView::Base says nothing about the available options.
So, as usual, we have to go digging through the source and hope they don't silently change the interface. Tracing through the code a bit yields this:
def tokenize(text, options)
options[:parent] = []
options[:attributes] ||= allowed_attributes
options[:tags] ||= allowed_tags
super
end
def process_node(node, result, options)
result << case node
when HTML::Tag
if node.closing == :close
options[:parent].shift
else
options[:parent].unshift node.name
end
process_attributes_for node, options
options[:tags].include?(node.name) ? node : nil
else
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<")
end
end
The default value for options[:tags] in tokenize and the way options[:tags] is used in process_node are of interest and tell us that if options[:tags] has anything then it has to include the entire set of allowed tags and there aren't any other options for controlling the tag set.
Also, if we look at sanitize_helper.rb, we see that sanitized_allowed_tags is just a wrapper for the allowed_tags in the whitelist sanitizer:
def sanitized_allowed_tags
white_list_sanitizer.allowed_tags
end
You should be able to add your own helper that does something like this (untested off-the-top-of-my-head code):
def sensible_sanitize(html, options)
if options.include? :not_tags
options[:tags] = ActionView::Base.sanitized_allowed_tags - options[:not_tags]
end
sanitize html, options
end
and then you could
<%= sensible_sanitize #stuff, :not_tags => [ 'div' ] %>
to use the standard default tags except for <div>.
I know you're looking for a way to pass the tags in, and for that mu's answer looks good!
I was keen to set them up globally which was trickier than I'd hoped as ActionView::Base has overridden sanitized_allowed_tags= to append rather than replace!
I ended up with the following to my application.rb:
SANITIZE_ALLOWED_TAGS = %w{a ul ol li b i}
config.after_initialize do
ActionView::Base.sanitized_allowed_tags.clear
ActionView::Base.sanitized_allowed_tags += SANITIZE_ALLOWED_TAGS
end

Ruby on Rails: Execute Logic Based on Selected Menu

I have a class that I use to contain select menu options for property types. It works fine. However, I need to be able to verify the selection and perform specific logic based on the selected option. This needs to happen in my Ruby code and in JavaScript.
Here is the class in question:
class PropertyTypes
def self.[](id)
##types[id]
end
def self.options_for_select
##for_select
end
private
##types = {
1 => "Residential",
2 => "Commercial",
3 => "Land",
4 => "Multi-Family",
5 => "Retail",
6 => "Shopping Center",
7 => "Industrial",
8 => "Self Storage",
9 => "Office",
10 => "Hospitality"
}
##for_select = ##types.each_pair.map{|id, display_name| [display_name, id]}
end
What is the best way to verify the selection? I need to perform specific logic and display user interface elements based on each type of property type.
Since I am storing the id, I would be verifying that the id is a particular property type. Something like:
PropertyTypes.isResidential?(id)
Then this method would look like this:
def self.isResidential?(id)
##types[id] == "Residential"
end
But now I am duplicating the string "Residential".
For JavaScript, I assume I would make an ajax call back to the model to keep the verification code DRY, but this seems like over kill.
Do I need to manually create a verification method for each property type or can I use define_method?
This seems so basic yet I am confused and burned out on this problem.
Thanks
===
Here's my solution:
class << self
##types.values.each do |v|
# need to remove any spaces or hashes from the found property type
v = v.downcase().gsub(/\W+/, '')
define_method "is_#{v}?", do |i|
type_name = ##types[i]
return false if type_name == nil #in case a bogus index is passed in
type_name = type_name.downcase().gsub(/\W+/, '')
type_name == v
end
end
end
It sounds like you can benefit from some Ruby meta-programming. Try googling "ruby method_missing". You can probably do something quick & dirty along the lines of:
class PropertyTypes
def method_missing(meth, *args, &block)
if meth.to_s =~ /^is_(.+)\?$/
##types[args.first] == $1
else
super
end
end
end
On the ruby side you could also use something like this to define dynamically these methods:
class << self
##types.values.each do |v|
define_method "is_#{v}?", do |i|
##types[i] == v
end
end
end

Resources