Modifying Action Text markup - Rails Tricks Issue 14

10 Jul 2023
Are you eager to elevate your security skills and safeguard your applications against cyber threats? I created a Rails Security course is designed specifically for developers like you who aim to build robust, secure Rails applications!
Buy my course: Security for Rails Developers.

Hi there,

I am working on a newsletter tool(Pombo) and this week, I want to share how I solved a problem I came across last week while working on it.

The problem: the newsletter tool needs to support importing past issues from other tools. These imported issues might contain references to images hosted at the other provider, and during the import, I need to move those images into Pombo because once the account on the old service is deleted, the images won’t be accessible anymore.

In Pombo, I use Action Text to store the body of a newsletter issue, and I use Active Storage for the images. My initial plan was to parse the HTML output of the Action Text field with Nokogiri, extract the image tags, create an Active Storage blob from them, and replace the image tag’s src in the markup to the blob’s URL.
Then I decided to look at Action Text’s source code to see how it works under the hood, and it turned out it will be way easier to do what I need than I expected.
An Action Text field has a “body” attribute, which is an instance of ActionText::Content which has a fragment method, which returns an instance of ActionText::Fragment and that’s a wrapper around the Nokogiri parsed markup.
Now putting this all together, to find all image tags on a content attribute backed by Action Text, we can just do the following:

content.body.fragment.find_all('img')

Next part is to download the image and create an ActiveStorage::Blob:

require 'open-uri'

content.body.fragment.find_all('img').each do |img|
  url = img.attribute('src').to_s # extract the src of the image
  io = URI.open(url) # download the file
  filename = File.basename(URI(url).path) # extract the filename from the URL
  # create the blob
  blob = ActiveStorage::Blob.create_and_upload! io: io, filename: filename
end

Next thing to figure out is how to replace the old image with the new one.
Digging a bit more into Action Text, I figured out I need to create an ActionText::Attachment from the blob and replace the nokogiri node with that:

attachment = ActionText::Attachment.from_attachable(
  blob,
  url: Rails.application.routes.url_helpers.rails_blob_url(blob)
)
img.replace attachment.node

And finally, we need to save the changes on the content. Here is the full snippet:

require "open-uri"
content.body.fragment.find_all('img').each do |img|
  url = img.attribute('src').to_s
  io = URI.open(url)
  filename = File.basename(URI(url).path)
  blob = ActiveStorage::Blob.create_and_upload! io: io, filename: filename
  attachment = ActionText::Attachment.from_attachable(
    blob,
    url: Rails.application.routes.url_helpers.rails_blob_url(blob)
  )
  img.replace attachment.node
end
content.save!

Based on what I learned from figuring out this solution, it will be pretty easy to add an outgoing link validator to make sure a newsletter doesn’t contain broken links.

I hope you enjoyed this, until next time.

Did you enjoy reading this? Sign up to the Rails Tricks newsletter for more content like this!

Or follow me on Twitter

Related posts