Wednesday, December 12, 2012

Render HTML file in Rails using Nokogiri

You may need to render static HTML template file from a controller action without modifying anything in to file. And, may need to replace some content during render.

A template may contain relative path for css, image etc. which may raise an exception as rails may not route that automatically. At that case you have to change the contents but that's not straightforward.

I have given an example below to render a static newsletter template placed in to public directory

Say, you have a newsletter template in to public/newsletters/1.html which contains something like below and want to render from newsletters#show:
    
    

    
{title}
{description}

Write a library to process newsletter template and placed in to lib
require 'nokogiri'
require 'uri'

module NewsletterProcessor
  TEMPLATE_DIR = "newsletter-template"
  TEMPLATE_PATH = "public/#{TEMPLATE_DIR}/1.html"

  def self.included(base)
    base.send(:before_filter, :load_newsletter, :only => :show)
  end

  def show
    content = render_to_string(:file => TEMPLATE_PATH)
    @content = TemplateParser.new(request, @newsletter.attributes).parse(content)
    render :text => @content, :layout => false
  end

  private

  def load_newsletter
    @newsletter = Newsletter.find(params[:id])
  end

  class TemplateParser
    cattr_accessor :request
    cattr_accessor :params

    def initialize(request, attrs)
      self.request = request
      self.params = attrs
    end

    def parse(content)
      content = Nokogiri::HTML(content)
      return parse_content(parse_assets_url(content))
    end

    private

    def parse_assets_url(content)
      #Parse Image URL
      content.css("img").each do |img|
        update_asset_attribute(img, "src")
      end

      #Parse Stylesheet URL
      content.css("link").each do |link|
        update_asset_attribute(link, "href")
      end

      return content
    end

    def parse_content(content)
      content = content.to_s
      content.gsub!(/\{(.*)\}/) do |exp|
        replace_attribute_value(exp)
      end

      return content
    end

    def replace_attribute_value(exp, attributes = self.params)
      key = exp.delete('{}').downcase.to_sym
      attributes.has_key?(key) ? attributes[key] : exp
    end

    def update_asset_attribute(ele, key)
      path = ele.attributes[key].value
      ele.attributes[key].value = full_url(path)
    end

    def full_url(path)
      url = URI.parse(path)
      return !url.scheme ? "#{self.request.scheme}://#{self.request.host_with_port}/#{TEMPLATE_DIR}/#{url.path}" : path
    end
  end
end

Now, just include the library in to the newsletters controller and run newsletters#show from browser.
class NewslettersController < ApplicationController
  include NewsletterProcessor
end
That's all!

3 comments:

  1. Nice post! Thank you Alam.

    ReplyDelete
  2. I was putting together something very similar.. thanks for posting this.. It gave me a better direction.

    ReplyDelete
  3. Thanks for posting this. I was just putting together something very similar. This gave me better direction.

    ReplyDelete