require 'rss/maker' require 'glue/markup' require 'rexml/document' require 'time' require 'uri' require 'facet/string/first_char' module Nitro # A helper that provides Feed related methods. # # To include this helper into your controller, # add the following at the beginning of your controller: # # helper :feed # # Then define actions that set an appropriate content_type and use build_(rss|atom|opml) # to generate your desired feed. See below for details. # # == RSS 0.91, 1.0 and 2.0 # # response.content_type = "application/rss+xml" # build_rss(og_objects, # :version => "0.9", # :base => context.host_url, # + object.to_href results in an item-link # :link => context.host_url+"/feed", # link to this feed # :title => "Feed Title", # :description => "What this feed is about", # :search_title => "Search Form Here", # :search_description => "Search description", # :search_input_name => "search_field", # :search_form_action => "http://url/to/search_action" # ) # # For RSS 1.0 or RSS 2.0 just change :version (defaults to '0.91'), # possible :version options are "0.9", "0.91", "1.0" and "2.0" # # * for RSS 0.9 :language is required (or defaults to 'en') # * for all RSS versions :title, :link and/or :base, :description are required # # individual objects have to respond to at least: # # * 1.0/0.9/2.0 require @title # * 1.0/0.9 require @to_href # * 2.0 requires @body # # if it doesn't, no item is created # # * @update_time, @create_time or @date is used for item.date # * so if Og's "is Timestamped" is being used, it'll be @update_time # * @author[:name] can optionally be used for item.author # # == Atom 1.0 # # response.content_type = "application/atom+xml" # build_atom(og_objects, # :title => "Feed Title", # :base => context.host_url, # + object.to_href results in an item-link # :link => context.host_url+"/atomfeed", # :id => "your_unique_id", # :base is being used unless :id specified (:base is recommended) # :author_name => "Takeo", # :author_email => "email@example.com", # :author_link => "http://uri.to/authors/home", # ) # # individual objects have to respond to at least: # # * @title # * @to_href # * @update_time/@create_time/@date (at least one of them) # # if it doesn't, no entry is created # # optional: # # * @body (taken as summary (256 chars)) # * @full_content # * use Og's "is Timestamped", so both @update_time and @create_time can be used # * @author[:name] # * @author[:link] # * @author[:email] # be careful, you don't want to publish your users email address to spammers # # # == OPML 1.0 feed lists # # Fabian: Eew, who invented OPML? Who needs it? Implementing it in a very rough way anyway though. # takes a Hash of Feeds and optional options # # response.content_type = "application/opml+xml" # build_opml( # { # "http://oxyliquit.de/feed" => "rss", # "http://oxyliquit.de/feed/questions" => "rss", # "http://oxyliquit.de/feed/tips" => "rss", # "http://oxyliquit.de/feed/tutorials" => "rss" # }, # :title => "My feeds" # ) module FeedHelper include Glue::Markup # RSS 0.91, 1.0, 2.0 feeds. def build_rss(objects, options = {}) # default options options = { :title => 'Syndication', :description => 'Syndication', :version => '0.9', :language => 'en', # required by 0.9 }.update(options) raise "Option ':version' contains a wrong version!" unless %w(0.9 0.91 1.0 2.0).include?(options[:version]) options[:base] ||= options[:link] raise "Option ':base' cannot be omitted!" unless options[:base] # build rss rss = RSS::Maker.make(options[:version]) do |maker| maker.channel.title = options[:title] maker.channel.description = options[:description] if options[:link] maker.channel.link = options[:link] else maker.channel.link = options[:base] #FIXME: not sure end case options[:version] when '0.9', '0.91' maker.channel.language = options[:language] when '1.0' if options[:link] maker.channel.about = options[:link] else raise "Option ':link' is required for RSS 1.0" end end maker.channel.generator = "Nitro " + Nitro::Version.to_s maker.items.do_sort = true # items for each object # * 1.0/0.9/2.0 require @title # * 1.0/0.9 require @link # * 2.0 requires @description objects.each do |o| # new Item item = maker.items.new_item # Link item.link = "#{options[:base]}/#{o.to_href}" if o.respond_to?(:to_href) item.guid.content = "#{options[:base]}/#{o.to_href}" if options[:version] == '2.0' && o.respond_to?(:to_href) # Title item.title = o.title if o.respond_to?(:title) # Description if o.respond_to? :body and body = o.body #TODO: think about whether markup should always be done # and whether 256 chars should be a fixed limit #item.description = markup(body.first_char(256)) # markup disabled, feedvalidator.org says "description should not contain HTML" # so removing everything that looks like a tag item.description = body.first_char(256).gsub!(/<[^>]+>/, ' ') end # Date (item.date asks for a Time object, so don't .to_s !) if o.respond_to?(:update_time) item.date = o.update_time elsif o.respond_to?(:create_time) item.date = o.create_time elsif o.respond_to?(:date) item.date = o.date end # Author if o.respond_to?(:author) if o.author[:name] item.author = o.author[:name] end end end if objects.size > 0 # objects/items # search form maker.textinput.title = options[:search_title] if options[:search_title] maker.textinput.description = options[:search_description] if options[:search_description] maker.textinput.name = options[:search_input_name] if options[:search_input_name] maker.textinput.link = options[:search_form_action] if options[:search_form_action] end return rss.to_s end # rss alias_method :rss, :build_rss # Atom 1.0 feeds. def build_atom(objects, options = {}) # default options options = { :title => 'Syndication', }.update(options) raise "first param must be a collection of objects!" unless objects.respond_to?(:to_ary) raise "your object(s) have to respond to :update_time, :create_time or :date" unless objects[0].respond_to?(:update_time) or objects[0].respond_to?(:create_time) or objects[0].respond_to?(:date) raise "Option ':base' cannot be omitted!" unless options[:base] # new XML Document for Atom atom = REXML::Document.new atom << REXML::XMLDecl.new("1.0", "utf-8") # Root element feed = REXML::Element.new("feed").add_namespace("http://www.w3.org/2005/Atom") # Required feed elements # id: Identifies the feed using a universally unique and permanent URI. iduri = URI.parse(options[:id] || options[:base]).normalize.to_s id = REXML::Element.new("id").add_text(iduri) feed << id # title: Contains a human readable title for the feed. title = REXML::Element.new("title").add_text(options[:title]) feed << title # updated: Indicates the last time the feed was modified in a significant way. latest = Time.at(0) # a while back objects.each do |o| if o.respond_to?(:update_time) latest = o.update_time if o.update_time > latest elsif o.respond_to?(:create_time) latest = o.create_time if o.create_time > latest elsif o.respond_to?(:date) latest = o.date if o.date > latest end end updated = REXML::Element.new("updated").add_text(latest.iso8601) feed << updated # Recommended feed elements # link: A feed should contain a link back to the feed itself. if options[:link] link = REXML::Element.new("link") link.add_attributes({ "rel" => "self", "href" => options[:link] }) feed << link end # author: Names one author of the feed. if options[:author_name] # name is required for author author = REXML::Element.new("author") author_name = REXML::Element.new("name").add_text(options[:author_name]) author << author_name if options[:author_email] author_email = REXML::Element.new("email").add_text(options[:author_email]) author << author_email end if options[:author_link] author_link = REXML::Element.new("uri").add_text(options[:author_link]) author << author_link end feed << author end # Optional feed elements # category: # contributor: # generator: Identifies the software used to generate the feed. generator = REXML::Element.new("generator") generator.add_attributes({ "uri" => "http://www.nitroproject.org", "version" => Nitro::Version }) generator.add_text("Nitro") feed << generator # icon # logo # rights # subtitle # Entries objects.each do |o| # new Entry (called "item" in RSS) unless o.respond_to?(:to_href) and o.respond_to?(:title) next end entry = REXML::Element.new("entry") # Required entry elements # id if o.respond_to?(:to_href) id = REXML::Element.new("id").add_text("#{options[:base]}/#{o.to_href}") entry << id end # title if o.respond_to?(:title) title = REXML::Element.new("title").add_text(o.title) entry << title end # updated updated = Time.at(0) # a while back if o.respond_to?(:update_time) updated = o.update_time elsif o.respond_to?(:create_time) updated = o.create_time elsif o.respond_to?(:date) updated = o.date end entry << REXML::Element.new("updated").add_text(updated.iso8601) # Recommended entry elements # author if o.respond_to?(:author) if o.author[:name] # name is required for author author = REXML::Element.new("author") author_name = REXML::Element.new("name").add_text(o.author[:name]) author << author_name if o.author[:email] author_email = REXML::Element.new("email").add_text(o.author[:email]) author << author_email end if o.author[:link] author_link = REXML::Element.new("uri").add_text(o.author[:link]) author << author_link end entry << author end end # summary if o.respond_to?(:body) summary = REXML::Element.new("summary") #TODO: think about whether 256 chars should be a fixed limit summary.add_text(o.body.first_char(256).gsub(/<[^>]+>/, ' ')) entry << summary end # content # may have the type text, html or xhtml if o.respond_to?(:full_content) content = REXML::Element.new("content") #TODO: think about whether markup should always be done content.add_text(markup(o.full_content)) entry << content end # link: An entry must contain an alternate link if there is no content element. if o.respond_to?(:to_href) link = REXML::Element.new("link") link.add_attributes({ "rel" => "alternate", "href" => "#{options[:base]}/#{o.to_href}" }) entry << link end # Optional entry elements # category # could be used for Tags maybe? # contributor # published if o.respond_to?(:create_time) published = REXML::Element.new("published") published.add_text(o.create_time.iso8601) entry << published end # source # rights # don't forget to add the entry to the feed feed << entry end if objects.size > 0 # objects/entries atom << feed return atom.to_s end # atom alias_method :atom, :build_atom # OPML 1.0 feed lists # Fabian: eww, who invented OPML? Who needs it? Implementing # it in a very rough way anyway though. Takes a Hash of # Feeds and optional options. def build_opml(feedhash, options = {}) # new XML Document for OPML opml = REXML::Document.new opml << REXML::XMLDecl.new("1.0", "utf-8") # Root element opml = REXML::Element.new("opml") opml.add_attribute("version", "1.0") # head head = REXML::Element.new("head") # title if options[:title] title = REXML::Element.new("title").add_text(options[:title]) head << title end # dateCreated # dateModified # ownerName # ownerEmail opml << head # body body = REXML::Element.new("body") feedhash.each do |url, type| outline = REXML::Element.new("outline") outline.add_attributes({ "type" => type, "xmlUrl" => url }) body << outline end opml << body return opml.to_s end # opml alias_method :opml, :build_opml end end # * Fabian Buch # * George Moschovitis