diff options
Diffstat (limited to 'lib/rss')
42 files changed, 5574 insertions, 1385 deletions
diff --git a/lib/rss/0.9.rb b/lib/rss/0.9.rb index eb2d828102..d852a6a85e 100644 --- a/lib/rss/0.9.rb +++ b/lib/rss/0.9.rb @@ -1,14 +1,48 @@ +# frozen_string_literal: false require "rss/parser" module RSS + ## + # = RSS 0.9 support + # + # RSS has three different versions. This module contains support for version + # 0.9.1[http://www.rssboard.org/rss-0-9-1-netscape]. + # + # == Producing RSS 0.9 + # + # Producing our own RSS feeds is easy as well. Let's make a very basic feed: + # + # require "rss" + # + # rss = RSS::Maker.make("0.91") do |maker| + # maker.channel.language = "en" + # maker.channel.author = "matz" + # maker.channel.updated = Time.now.to_s + # maker.channel.link = "http://www.ruby-lang.org/en/feeds/news.rss" + # maker.channel.title = "Example Feed" + # maker.channel.description = "A longer description of my feed." + # maker.image.url = "http://www.ruby-lang.org/images/logo.gif" + # maker.image.title = "An image" + # maker.items.new_item do |item| + # item.link = "http://www.ruby-lang.org/en/news/2010/12/25/ruby-1-9-2-p136-is-released/" + # item.title = "Ruby 1.9.2-p136 is released" + # item.updated = Time.now.to_s + # end + # end + # + # puts rss + # + # As you can see, this is a very Builder-like DSL. This code will spit out an + # RSS 0.9 feed with one item. If we needed a second item, we'd make another + # block with maker.items.new_item and build a second one. module RSS09 NSPOOL = {} ELEMENTS = [] def self.append_features(klass) super - + klass.install_must_call_validator('', "") end end @@ -17,16 +51,18 @@ module RSS include RSS09 include RootElementMixin - include XMLStyleSheetMixin %w(channel).each do |name| install_have_child_element(name, "", nil) end - attr_accessor :rss_version, :version, :encoding, :standalone - - def initialize(rss_version, version=nil, encoding=nil, standalone=nil) + attr_writer :feed_version + alias_method(:rss_version, :feed_version) + alias_method(:rss_version=, :feed_version=) + + def initialize(feed_version, version=nil, encoding=nil, standalone=nil) super + @feed_type = "rss" end def items @@ -58,12 +94,14 @@ module RSS items.each do |item| item.setup_maker(maker.items) end + image.setup_maker(maker) if image + textinput.setup_maker(maker) if textinput end private def _attrs [ - ["version", true, "rss_version"], + ["version", true, "feed_version"], ] end @@ -119,7 +157,7 @@ module RSS def not_need_to_call_setup_maker_variables %w(image textInput) end - + class SkipDays < Element include RSS09 @@ -142,11 +180,11 @@ module RSS self.content = args[0] end end - + end - + end - + class SkipHours < Element include RSS09 @@ -170,13 +208,13 @@ module RSS end end end - + end - + class Image < Element include RSS09 - + %w(url title link).each do |name| install_text_element(name, "", nil) end @@ -235,9 +273,9 @@ module RSS end end end - + class Item < Element - + include RSS09 [ @@ -265,7 +303,7 @@ module RSS @enclosure.setup_maker(item) if @enclosure @source.setup_maker(item) if @source end - + class Source < Element include RSS09 @@ -275,7 +313,7 @@ module RSS ].each do |name, uri, required| install_get_attribute(name, uri, required) end - + content_setup def initialize(*args) @@ -337,7 +375,7 @@ module RSS class Category < Element include RSS09 - + [ ["domain", "", false] ].each do |name, uri, required| @@ -365,11 +403,11 @@ module RSS category.domain = domain category.content = content end - + end end - + class TextInput < Element include RSS09 @@ -395,29 +433,30 @@ module RSS maker.textinput end end - + end - + end RSS09::ELEMENTS.each do |name| - BaseListener.install_get_text_element("", name, "#{name}=") + BaseListener.install_get_text_element("", name, name) end module ListenerMixin private - def start_rss(tag_name, prefix, attrs, ns) - check_ns(tag_name, prefix, ns, "") - + def initial_start_rss(tag_name, prefix, attrs, ns) + check_ns(tag_name, prefix, ns, "", false) + @rss = Rss.new(attrs['version'], @version, @encoding, @standalone) @rss.do_validate = @do_validate @rss.xml_stylesheets = @xml_stylesheets @last_element = @rss - @proc_stack.push Proc.new { |text, tags| + pr = Proc.new do |text, tags| @rss.validate_for_stream(tags, @ignore_unknown_element) if @do_validate - } + end + @proc_stack.push(pr) end - + end end diff --git a/lib/rss/1.0.rb b/lib/rss/1.0.rb index a945434fbf..fb63937c5e 100644 --- a/lib/rss/1.0.rb +++ b/lib/rss/1.0.rb @@ -1,14 +1,47 @@ +# frozen_string_literal: false require "rss/parser" module RSS + ## + # = RSS 1.0 support + # + # RSS has three different versions. This module contains support for version + # 1.0[http://web.resource.org/rss/1.0/] + # + # == Producing RSS 1.0 + # + # Producing our own RSS feeds is easy as well. Let's make a very basic feed: + # + # require "rss" + # + # rss = RSS::Maker.make("1.0") do |maker| + # maker.channel.language = "en" + # maker.channel.author = "matz" + # maker.channel.about = "About my feed." + # maker.channel.updated = Time.now.to_s + # maker.channel.link = "http://www.ruby-lang.org/en/feeds/news.rss" + # maker.channel.title = "Example Feed" + # maker.channel.description = "A longer description of my feed." + # maker.items.new_item do |item| + # item.link = "http://www.ruby-lang.org/en/news/2010/12/25/ruby-1-9-2-p136-is-released/" + # item.title = "Ruby 1.9.2-p136 is released" + # item.updated = Time.now.to_s + # end + # end + # + # puts rss + # + # As you can see, this is a very Builder-like DSL. This code will spit out an + # RSS 1.0 feed with one item. If we needed a second item, we'd make another + # block with maker.items.new_item and build a second one. module RSS10 NSPOOL = {} ELEMENTS = [] def self.append_features(klass) super - + klass.install_must_call_validator('', ::RSS::URI) end @@ -45,10 +78,10 @@ module RSS __send__("install_have_#{type}_element", tag, ::RSS::URI, occurs) end - attr_accessor :rss_version, :version, :encoding, :standalone - + alias_method(:rss_version, :feed_version) def initialize(version=nil, encoding=nil, standalone=nil) super('1.0', version, encoding, standalone) + @feed_type = "rss" end def full_name @@ -64,13 +97,13 @@ module RSS URI end end - + [ ["resource", [URI, ""], true] ].each do |name, uri, required| install_get_attribute(name, uri, required) end - + def initialize(*args) if Utils.element_initialize_arguments?(args) super @@ -98,10 +131,10 @@ module RSS end @tag_name = 'Seq' - + install_have_children_element("li", URI, "*") install_must_call_validator('rdf', ::RSS::RDF::URI) - + def initialize(*args) if Utils.element_initialize_arguments?(args) super @@ -114,7 +147,7 @@ module RSS def full_name tag_name_with_prefix(PREFIX) end - + def setup_maker(target) lis.each do |li| target << li.resource @@ -135,10 +168,10 @@ module RSS end @tag_name = 'Bag' - + install_have_children_element("li", URI, "*") install_must_call_validator('rdf', URI) - + def initialize(*args) if Utils.element_initialize_arguments?(args) super @@ -151,7 +184,7 @@ module RSS def full_name tag_name_with_prefix(PREFIX) end - + def setup_maker(target) lis.each do |li| target << li.resource @@ -162,7 +195,7 @@ module RSS class Channel < Element include RSS10 - + class << self def required_uri @@ -202,17 +235,17 @@ module RSS def maker_target(maker) maker.channel end - + def setup_maker_attributes(channel) channel.about = about end class Image < Element - + include RSS10 class << self - + def required_uri ::RSS::URI end @@ -225,7 +258,7 @@ module RSS install_get_attribute(name, uri, required, nil, nil, "#{PREFIX}:#{name}") end - + def initialize(*args) if Utils.element_initialize_arguments?(args) super @@ -237,11 +270,11 @@ module RSS end class Textinput < Element - + include RSS10 class << self - + def required_uri ::RSS::URI end @@ -254,7 +287,7 @@ module RSS install_get_attribute(name, uri, required, nil, nil, "#{PREFIX}:#{name}") end - + def initialize(*args) if Utils.element_initialize_arguments?(args) super @@ -264,7 +297,7 @@ module RSS end end end - + class Items < Element include RSS10 @@ -272,16 +305,16 @@ module RSS Seq = ::RSS::RDF::Seq class << self - + def required_uri ::RSS::URI end - + end install_have_child_element("Seq", URI, nil) install_must_call_validator('rdf', URI) - + def initialize(*args) if Utils.element_initialize_arguments?(args) super @@ -309,7 +342,7 @@ module RSS include RSS10 class << self - + def required_uri ::RSS::URI end @@ -351,7 +384,7 @@ module RSS def required_uri ::RSS::URI end - + end @@ -430,21 +463,22 @@ module RSS end RSS10::ELEMENTS.each do |name| - BaseListener.install_get_text_element(URI, name, "#{name}=") + BaseListener.install_get_text_element(URI, name, name) end module ListenerMixin private - def start_RDF(tag_name, prefix, attrs, ns) - check_ns(tag_name, prefix, ns, RDF::URI) + def initial_start_RDF(tag_name, prefix, attrs, ns) + check_ns(tag_name, prefix, ns, RDF::URI, false) @rss = RDF.new(@version, @encoding, @standalone) @rss.do_validate = @do_validate @rss.xml_stylesheets = @xml_stylesheets @last_element = @rss - @proc_stack.push Proc.new { |text, tags| + pr = Proc.new do |text, tags| @rss.validate_for_stream(tags, @ignore_unknown_element) if @do_validate - } + end + @proc_stack.push(pr) end end diff --git a/lib/rss/2.0.rb b/lib/rss/2.0.rb index 44bdb4f1ae..13f9ade918 100644 --- a/lib/rss/2.0.rb +++ b/lib/rss/2.0.rb @@ -1,7 +1,39 @@ +# frozen_string_literal: false require "rss/0.9" module RSS + ## + # = RSS 2.0 support + # + # RSS has three different versions. This module contains support for version + # 2.0[http://www.rssboard.org/rss-specification] + # + # == Producing RSS 2.0 + # + # Producing our own RSS feeds is easy as well. Let's make a very basic feed: + # + # require "rss" + # + # rss = RSS::Maker.make("2.0") do |maker| + # maker.channel.language = "en" + # maker.channel.author = "matz" + # maker.channel.updated = Time.now.to_s + # maker.channel.link = "http://www.ruby-lang.org/en/feeds/news.rss" + # maker.channel.title = "Example Feed" + # maker.channel.description = "A longer description of my feed." + # maker.items.new_item do |item| + # item.link = "http://www.ruby-lang.org/en/news/2010/12/25/ruby-1-9-2-p136-is-released/" + # item.title = "Ruby 1.9.2-p136 is released" + # item.updated = Time.now.to_s + # end + # end + # + # puts rss + # + # As you can see, this is a very Builder-like DSL. This code will spit out an + # RSS 2.0 feed with one item. If we needed a second item, we'd make another + # block with maker.items.new_item and build a second one. class Rss class Channel @@ -29,7 +61,7 @@ module RSS Category = Item::Category class Item - + [ ["comments", "?"], ["author", "?"], @@ -57,9 +89,9 @@ module RSS _setup_maker_element(item) @guid.setup_maker(item) if @guid end - + class Guid < Element - + include RSS09 [ @@ -105,7 +137,7 @@ module RSS end RSS09::ELEMENTS.each do |name| - BaseListener.install_get_text_element("", name, "#{name}=") + BaseListener.install_get_text_element("", name, name) end end diff --git a/lib/rss/atom.rb b/lib/rss/atom.rb new file mode 100644 index 0000000000..38e927478c --- /dev/null +++ b/lib/rss/atom.rb @@ -0,0 +1,1025 @@ +# frozen_string_literal: false +require 'rss/parser' + +module RSS + ## + # Atom is an XML-based document format that is used to describe 'feeds' of related information. + # A typical use is in a news feed where the information is periodically updated and which users + # can subscribe to. The Atom format is described in http://tools.ietf.org/html/rfc4287 + # + # The Atom module provides support in reading and creating feeds. + # + # See the RSS module for examples consuming and creating feeds. + module Atom + + ## + # The Atom URI W3C Namespace + + URI = "http://www.w3.org/2005/Atom" + + ## + # The XHTML URI W3C Namespace + + XHTML_URI = "http://www.w3.org/1999/xhtml" + + module CommonModel + NSPOOL = {} + ELEMENTS = [] + + def self.append_features(klass) + super + klass.install_must_call_validator("atom", URI) + [ + ["lang", :xml], + ["base", :xml], + ].each do |name, uri, required| + klass.install_get_attribute(name, uri, required, [nil, :inherit]) + end + klass.class_eval do + class << self + # Returns the Atom URI W3C Namespace + def required_uri + URI + end + + # Returns true + def need_parent? + true + end + end + end + end + end + + module ContentModel + module ClassMethods + def content_type + @content_type ||= nil + end + end + + class << self + def append_features(klass) + super + klass.extend(ClassMethods) + klass.content_setup(klass.content_type, klass.tag_name) + end + end + + def maker_target(target) + target + end + + private + def setup_maker_element_writer + "#{self.class.name.split(/::/).last.downcase}=" + end + + def setup_maker_element(target) + target.__send__(setup_maker_element_writer, content) + super + end + end + + module URIContentModel + class << self + def append_features(klass) + super + klass.class_eval do + @content_type = [nil, :uri] + include(ContentModel) + end + end + end + end + + # The TextConstruct module is used to define a Text construct Atom element, + # which is used to store small quantities of human-readable text. + # + # The TextConstruct has a type attribute, e.g. text, html, xhtml + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#text.constructs + module TextConstruct + def self.append_features(klass) + super + klass.class_eval do + [ + ["type", ""], + ].each do |name, uri, required| + install_get_attribute(name, uri, required, :text_type) + end + + content_setup + add_need_initialize_variable("xhtml") + + class << self + def xml_getter + "xhtml" + end + + def xml_setter + "xhtml=" + end + end + end + end + + attr_writer :xhtml + + # Returns or builds the XHTML content. + def xhtml + return @xhtml if @xhtml.nil? + if @xhtml.is_a?(XML::Element) and + [@xhtml.name, @xhtml.uri] == ["div", XHTML_URI] + return @xhtml + end + + children = @xhtml + children = [children] unless children.is_a?(Array) + XML::Element.new("div", nil, XHTML_URI, + {"xmlns" => XHTML_URI}, children) + end + + # Returns true if type is "xhtml". + def have_xml_content? + @type == "xhtml" + end + + # Raises a MissingTagError or NotExpectedTagError + # if the element is not properly formatted. + def atom_validate(ignore_unknown_element, tags, uri) + if have_xml_content? + if @xhtml.nil? + raise MissingTagError.new("div", tag_name) + end + unless [@xhtml.name, @xhtml.uri] == ["div", XHTML_URI] + raise NotExpectedTagError.new(@xhtml.name, @xhtml.uri, tag_name) + end + end + end + + private + def maker_target(target) + target.__send__(self.class.name.split(/::/).last.downcase) {|x| x} + end + + def setup_maker_attributes(target) + target.type = type + target.content = content + target.xml_content = @xhtml + end + end + + # The PersonConstruct module is used to define a person Atom element that can be + # used to describe a person, corporation or similar entity. + # + # The PersonConstruct has a Name, Uri and Email child elements. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#atomPersonConstruct + module PersonConstruct + + # Adds attributes for name, uri, and email to the +klass+ + def self.append_features(klass) + super + klass.class_eval do + [ + ["name", nil], + ["uri", "?"], + ["email", "?"], + ].each do |tag, occurs| + install_have_attribute_element(tag, URI, occurs, nil, :content) + end + end + end + + def maker_target(target) + target.__send__("new_#{self.class.name.split(/::/).last.downcase}") + end + + # The name of the person or entity. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.name + class Name < RSS::Element + include CommonModel + include ContentModel + end + + # The URI of the person or entity. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.uri + class Uri < RSS::Element + include CommonModel + include URIContentModel + end + + # The email of the person or entity. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.email + class Email < RSS::Element + include CommonModel + include ContentModel + end + end + + # Element used to describe an Atom date and time in the ISO 8601 format + # + # Examples: + # * 2013-03-04T15:30:02Z + # * 2013-03-04T10:30:02-05:00 + module DateConstruct + def self.append_features(klass) + super + klass.class_eval do + @content_type = :w3cdtf + include(ContentModel) + end + end + + # Raises NotAvailableValueError if element content is nil + def atom_validate(ignore_unknown_element, tags, uri) + raise NotAvailableValueError.new(tag_name, "") if content.nil? + end + end + + module DuplicateLinkChecker + # Checks if there are duplicate links with the same type and hreflang attributes + # that have an alternate (or empty) rel attribute + # + # Raises a TooMuchTagError if there are duplicates found + def validate_duplicate_links(links) + link_infos = {} + links.each do |link| + rel = link.rel || "alternate" + next unless rel == "alternate" + key = [link.hreflang, link.type] + if link_infos.has_key?(key) + raise TooMuchTagError.new("link", tag_name) + end + link_infos[key] = true + end + end + end + + # Defines the top-level element of an Atom Feed Document. + # It consists of a number of children Entry elements, + # and has the following attributes: + # + # * author + # * categories + # * category + # * content + # * contributor + # * entries (aliased as items) + # * entry + # * generator + # * icon + # * id + # * link + # * logo + # * rights + # * subtitle + # * title + # * updated + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.feed + class Feed < RSS::Element + include RootElementMixin + include CommonModel + include DuplicateLinkChecker + + install_ns('', URI) + + [ + ["author", "*", :children], + ["category", "*", :children, "categories"], + ["contributor", "*", :children], + ["generator", "?"], + ["icon", "?", nil, :content], + ["id", nil, nil, :content], + ["link", "*", :children], + ["logo", "?"], + ["rights", "?"], + ["subtitle", "?", nil, :content], + ["title", nil, nil, :content], + ["updated", nil, nil, :content], + ["entry", "*", :children, "entries"], + ].each do |tag, occurs, type, *args| + type ||= :child + __send__("install_have_#{type}_element", + tag, URI, occurs, tag, *args) + end + + # Creates a new Atom feed + def initialize(version=nil, encoding=nil, standalone=nil) + super("1.0", version, encoding, standalone) + @feed_type = "atom" + @feed_subtype = "feed" + end + + alias_method :items, :entries + + # Returns true if there are any authors for the feed or any of the Entry + # child elements have an author + def have_author? + authors.any? {|author| !author.to_s.empty?} or + entries.any? {|entry| entry.have_author?(false)} + end + + private + def atom_validate(ignore_unknown_element, tags, uri) + unless have_author? + raise MissingTagError.new("author", tag_name) + end + validate_duplicate_links(links) + end + + def have_required_elements? + super and have_author? + end + + def maker_target(maker) + maker.channel + end + + def setup_maker_element(channel) + prev_dc_dates = channel.dc_dates.to_a.dup + super + channel.about = id.content if id + channel.dc_dates.replace(prev_dc_dates) + end + + def setup_maker_elements(channel) + super + items = channel.maker.items + entries.each do |entry| + entry.setup_maker(items) + end + end + + # PersonConstruct that contains information regarding the author + # of a Feed or Entry. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.author + class Author < RSS::Element + include CommonModel + include PersonConstruct + end + + # Contains information about a category associated with a Feed or Entry. + # It has the following attributes: + # + # * term + # * scheme + # * label + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.category + class Category < RSS::Element + include CommonModel + + [ + ["term", "", true], + ["scheme", "", false, [nil, :uri]], + ["label", ""], + ].each do |name, uri, required, type| + install_get_attribute(name, uri, required, type) + end + + private + def maker_target(target) + target.new_category + end + end + + # PersonConstruct that contains information regarding the + # contributors of a Feed or Entry. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.contributor + class Contributor < RSS::Element + include CommonModel + include PersonConstruct + end + + # Contains information on the agent used to generate the feed. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.generator + class Generator < RSS::Element + include CommonModel + include ContentModel + + [ + ["uri", "", false, [nil, :uri]], + ["version", ""], + ].each do |name, uri, required, type| + install_get_attribute(name, uri, required, type) + end + + private + def setup_maker_attributes(target) + target.generator do |generator| + generator.uri = uri if uri + generator.version = version if version + end + end + end + + # Defines an image that provides a visual identification for a eed. + # The image should have an aspect ratio of 1:1. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.icon + class Icon < RSS::Element + include CommonModel + include URIContentModel + end + + # Defines the Universally Unique Identifier (UUID) for a Feed or Entry. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.id + class Id < RSS::Element + include CommonModel + include URIContentModel + end + + # Defines a reference to a Web resource. It has the following + # attributes: + # + # * href + # * rel + # * type + # * hreflang + # * title + # * length + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.link + class Link < RSS::Element + include CommonModel + + [ + ["href", "", true, [nil, :uri]], + ["rel", ""], + ["type", ""], + ["hreflang", ""], + ["title", ""], + ["length", ""], + ].each do |name, uri, required, type| + install_get_attribute(name, uri, required, type) + end + + private + def maker_target(target) + target.new_link + end + end + + # Defines an image that provides a visual identification for the Feed. + # The image should have an aspect ratio of 2:1 (horizontal:vertical). + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.logo + class Logo < RSS::Element + include CommonModel + include URIContentModel + + def maker_target(target) + target.maker.image + end + + private + def setup_maker_element_writer + "url=" + end + end + + # TextConstruct that contains copyright information regarding + # the content in an Entry or Feed. It should not be used to + # convey machine readable licensing information. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.rights + class Rights < RSS::Element + include CommonModel + include TextConstruct + end + + # TextConstruct that conveys a description or subtitle for a Feed. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.subtitle + class Subtitle < RSS::Element + include CommonModel + include TextConstruct + end + + # TextConstruct that conveys a description or title for a Feed or Entry. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.title + class Title < RSS::Element + include CommonModel + include TextConstruct + end + + # DateConstruct indicating the most recent time when a Feed or + # Entry was modified in a way the publisher considers + # significant. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.updated + class Updated < RSS::Element + include CommonModel + include DateConstruct + end + + # Defines a child Atom Entry element of an Atom Feed element. + # It has the following attributes: + # + # * author + # * category + # * categories + # * content + # * contributor + # * id + # * link + # * published + # * rights + # * source + # * summary + # * title + # * updated + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.entry + class Entry < RSS::Element + include CommonModel + include DuplicateLinkChecker + + [ + ["author", "*", :children], + ["category", "*", :children, "categories"], + ["content", "?", :child], + ["contributor", "*", :children], + ["id", nil, nil, :content], + ["link", "*", :children], + ["published", "?", :child, :content], + ["rights", "?", :child], + ["source", "?"], + ["summary", "?", :child], + ["title", nil], + ["updated", nil, :child, :content], + ].each do |tag, occurs, type, *args| + type ||= :attribute + __send__("install_have_#{type}_element", + tag, URI, occurs, tag, *args) + end + + # Returns whether any of the following are true: + # + # * There are any authors in the feed + # * If the parent element has an author and the +check_parent+ + # parameter was given. + # * There is a source element that has an author + def have_author?(check_parent=true) + authors.any? {|author| !author.to_s.empty?} or + (check_parent and @parent and @parent.have_author?) or + (source and source.have_author?) + end + + private + def atom_validate(ignore_unknown_element, tags, uri) + unless have_author? + raise MissingTagError.new("author", tag_name) + end + validate_duplicate_links(links) + end + + def have_required_elements? + super and have_author? + end + + def maker_target(items) + if items.respond_to?("items") + # For backward compatibility + items = items.items + end + items.new_item + end + + # Feed::Author + Author = Feed::Author + # Feed::Category + Category = Feed::Category + + # Contains or links to the content of the Entry. + # It has the following attributes: + # + # * type + # * src + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.content + class Content < RSS::Element + include CommonModel + + class << self + def xml_setter + "xml=" + end + + def xml_getter + "xml" + end + end + + [ + ["type", ""], + ["src", "", false, [nil, :uri]], + ].each do |name, uri, required, type| + install_get_attribute(name, uri, required, type) + end + + content_setup + add_need_initialize_variable("xml") + + # Returns the element content in XML. + attr_writer :xml + + # Returns true if the element has inline XML content. + def have_xml_content? + inline_xhtml? or inline_other_xml? + end + + # Returns or builds the element content in XML. + def xml + return @xml unless inline_xhtml? + return @xml if @xml.nil? + if @xml.is_a?(XML::Element) and + [@xml.name, @xml.uri] == ["div", XHTML_URI] + return @xml + end + + children = @xml + children = [children] unless children.is_a?(Array) + XML::Element.new("div", nil, XHTML_URI, + {"xmlns" => XHTML_URI}, children) + end + + # Returns the element content in XHTML. + def xhtml + if inline_xhtml? + xml + else + nil + end + end + + # Raises a MissingAttributeError, NotAvailableValueError, + # MissingTagError or NotExpectedTagError if the element is + # not properly formatted. + def atom_validate(ignore_unknown_element, tags, uri) + if out_of_line? + raise MissingAttributeError.new(tag_name, "type") if @type.nil? + unless (content.nil? or content.empty?) + raise NotAvailableValueError.new(tag_name, content) + end + elsif inline_xhtml? + if @xml.nil? + raise MissingTagError.new("div", tag_name) + end + unless @xml.name == "div" and @xml.uri == XHTML_URI + raise NotExpectedTagError.new(@xml.name, @xml.uri, tag_name) + end + end + end + + # Returns true if the element contains inline content + # that has a text or HTML media type, or no media type at all. + def inline_text? + !out_of_line? and [nil, "text", "html"].include?(@type) + end + + # Returns true if the element contains inline content that + # has a HTML media type. + def inline_html? + return false if out_of_line? + @type == "html" or mime_split == ["text", "html"] + end + + # Returns true if the element contains inline content that + # has a XHTML media type. + def inline_xhtml? + !out_of_line? and @type == "xhtml" + end + + # Returns true if the element contains inline content that + # has a MIME media type. + def inline_other? + return false if out_of_line? + media_type, subtype = mime_split + return false if media_type.nil? or subtype.nil? + true + end + + # Returns true if the element contains inline content that + # has a text media type. + def inline_other_text? + return false unless inline_other? + return false if inline_other_xml? + + media_type, = mime_split + return true if "text" == media_type.downcase + false + end + + # Returns true if the element contains inline content that + # has a XML media type. + def inline_other_xml? + return false unless inline_other? + + media_type, subtype = mime_split + normalized_mime_type = "#{media_type}/#{subtype}".downcase + if /(?:\+xml|^xml)$/ =~ subtype or + %w(text/xml-external-parsed-entity + application/xml-external-parsed-entity + application/xml-dtd).find {|x| x == normalized_mime_type} + return true + end + false + end + + # Returns true if the element contains inline content + # encoded in base64. + def inline_other_base64? + inline_other? and !inline_other_text? and !inline_other_xml? + end + + # Returns true if the element contains linked content. + def out_of_line? + not @src.nil? + end + + # Splits the type attribute into an array, e.g. ["text", "xml"] + def mime_split + media_type = subtype = nil + if /\A\s*([a-z]+)\/([a-z\+]+)\s*(?:;.*)?\z/i =~ @type.to_s + media_type = $1.downcase + subtype = $2.downcase + end + [media_type, subtype] + end + + # Returns true if the content needs to be encoded in base64. + def need_base64_encode? + inline_other_base64? + end + + private + def empty_content? + out_of_line? or super + end + end + + # Feed::Contributor + Contributor = Feed::Contributor + # Feed::Id + Id = Feed::Id + # Feed::Link + Link = Feed::Link + + # DateConstruct that usually indicates the time of the initial + # creation of an Entry. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.published + class Published < RSS::Element + include CommonModel + include DateConstruct + end + + # Feed::Rights + Rights = Feed::Rights + + # Defines a Atom Source element. It has the following attributes: + # + # * author + # * category + # * categories + # * content + # * contributor + # * generator + # * icon + # * id + # * link + # * logo + # * rights + # * subtitle + # * title + # * updated + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.source + class Source < RSS::Element + include CommonModel + + [ + ["author", "*", :children], + ["category", "*", :children, "categories"], + ["contributor", "*", :children], + ["generator", "?"], + ["icon", "?"], + ["id", "?", nil, :content], + ["link", "*", :children], + ["logo", "?"], + ["rights", "?"], + ["subtitle", "?"], + ["title", "?"], + ["updated", "?", nil, :content], + ].each do |tag, occurs, type, *args| + type ||= :attribute + __send__("install_have_#{type}_element", + tag, URI, occurs, tag, *args) + end + + # Returns true if the Source element has an author. + def have_author? + !author.to_s.empty? + end + + # Feed::Author + Author = Feed::Author + # Feed::Category + Category = Feed::Category + # Feed::Contributor + Contributor = Feed::Contributor + # Feed::Generator + Generator = Feed::Generator + # Feed::Icon + Icon = Feed::Icon + # Feed::Id + Id = Feed::Id + # Feed::Link + Link = Feed::Link + # Feed::Logo + Logo = Feed::Logo + # Feed::Rights + Rights = Feed::Rights + # Feed::Subtitle + Subtitle = Feed::Subtitle + # Feed::Title + Title = Feed::Title + # Feed::Updated + Updated = Feed::Updated + end + + # TextConstruct that describes a summary of the Entry. + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.summary + class Summary < RSS::Element + include CommonModel + include TextConstruct + end + + # Feed::Title + Title = Feed::Title + # Feed::Updated + Updated = Feed::Updated + end + end + + # Defines a top-level Atom Entry element, + # used as the document element of a stand-alone Atom Entry Document. + # It has the following attributes: + # + # * author + # * category + # * categories + # * content + # * contributor + # * id + # * link + # * published + # * rights + # * source + # * summary + # * title + # * updated + # + # Reference: https://validator.w3.org/feed/docs/rfc4287.html#element.entry] + class Entry < RSS::Element + include RootElementMixin + include CommonModel + include DuplicateLinkChecker + + [ + ["author", "*", :children], + ["category", "*", :children, "categories"], + ["content", "?"], + ["contributor", "*", :children], + ["id", nil, nil, :content], + ["link", "*", :children], + ["published", "?", :child, :content], + ["rights", "?"], + ["source", "?"], + ["summary", "?"], + ["title", nil], + ["updated", nil, nil, :content], + ].each do |tag, occurs, type, *args| + type ||= :attribute + __send__("install_have_#{type}_element", + tag, URI, occurs, tag, *args) + end + + # Creates a new Atom Entry element. + def initialize(version=nil, encoding=nil, standalone=nil) + super("1.0", version, encoding, standalone) + @feed_type = "atom" + @feed_subtype = "entry" + end + + # Returns the Entry in an array. + def items + [self] + end + + # Sets up the +maker+ for constructing Entry elements. + def setup_maker(maker) + maker = maker.maker if maker.respond_to?("maker") + super(maker) + end + + # Returns where there are any authors present or there is a + # source with an author. + def have_author? + authors.any? {|author| !author.to_s.empty?} or + (source and source.have_author?) + end + + private + def atom_validate(ignore_unknown_element, tags, uri) + unless have_author? + raise MissingTagError.new("author", tag_name) + end + validate_duplicate_links(links) + end + + def have_required_elements? + super and have_author? + end + + def maker_target(maker) + maker.items.new_item + end + + # Feed::Entry::Author + Author = Feed::Entry::Author + # Feed::Entry::Category + Category = Feed::Entry::Category + # Feed::Entry::Content + Content = Feed::Entry::Content + # Feed::Entry::Contributor + Contributor = Feed::Entry::Contributor + # Feed::Entry::Id + Id = Feed::Entry::Id + # Feed::Entry::Link + Link = Feed::Entry::Link + # Feed::Entry::Published + Published = Feed::Entry::Published + # Feed::Entry::Rights + Rights = Feed::Entry::Rights + # Feed::Entry::Source + Source = Feed::Entry::Source + # Feed::Entry::Summary + Summary = Feed::Entry::Summary + # Feed::Entry::Title + Title = Feed::Entry::Title + # Feed::Entry::Updated + Updated = Feed::Entry::Updated + end + end + + Atom::CommonModel::ELEMENTS.each do |name| + BaseListener.install_get_text_element(Atom::URI, name, "#{name}=") + end + + module ListenerMixin + private + def initial_start_feed(tag_name, prefix, attrs, ns) + check_ns(tag_name, prefix, ns, Atom::URI, false) + + @rss = Atom::Feed.new(@version, @encoding, @standalone) + @rss.do_validate = @do_validate + @rss.xml_stylesheets = @xml_stylesheets + @rss.lang = attrs["xml:lang"] + @rss.base = attrs["xml:base"] + @last_element = @rss + pr = Proc.new do |text, tags| + @rss.validate_for_stream(tags) if @do_validate + end + @proc_stack.push(pr) + end + + def initial_start_entry(tag_name, prefix, attrs, ns) + check_ns(tag_name, prefix, ns, Atom::URI, false) + + @rss = Atom::Entry.new(@version, @encoding, @standalone) + @rss.do_validate = @do_validate + @rss.xml_stylesheets = @xml_stylesheets + @rss.lang = attrs["xml:lang"] + @rss.base = attrs["xml:base"] + @last_element = @rss + pr = Proc.new do |text, tags| + @rss.validate_for_stream(tags) if @do_validate + end + @proc_stack.push(pr) + end + end +end diff --git a/lib/rss/content.rb b/lib/rss/content.rb index 1b13f39fcf..d35311075a 100644 --- a/lib/rss/content.rb +++ b/lib/rss/content.rb @@ -1,38 +1,34 @@ -require "rss/1.0" +# frozen_string_literal: false +require "rss/rss" module RSS - + # The prefix for the Content XML namespace. CONTENT_PREFIX = 'content' + # The URI of the Content specification. CONTENT_URI = "http://purl.org/rss/1.0/modules/content/" - RDF.install_ns(CONTENT_PREFIX, CONTENT_URI) - module ContentModel - extend BaseModel - ELEMENTS = [] + ELEMENTS = ["#{CONTENT_PREFIX}_encoded"] def self.append_features(klass) super klass.install_must_call_validator(CONTENT_PREFIX, CONTENT_URI) - %w(encoded).each do |name| - klass.install_text_element(name, CONTENT_URI, "?", - "#{CONTENT_PREFIX}_#{name}") + ELEMENTS.each do |full_name| + name = full_name[(CONTENT_PREFIX.size + 1)..-1] + klass.install_text_element(name, CONTENT_URI, "?", full_name) end end end - class RDF - class Item; include ContentModel; end - end - prefix_size = CONTENT_PREFIX.size + 1 - ContentModel::ELEMENTS.uniq! ContentModel::ELEMENTS.each do |full_name| name = full_name[prefix_size..-1] - BaseListener.install_get_text_element(CONTENT_URI, name, "#{full_name}=") + BaseListener.install_get_text_element(CONTENT_URI, name, full_name) end - end + +require 'rss/content/1.0' +require 'rss/content/2.0' diff --git a/lib/rss/content/1.0.rb b/lib/rss/content/1.0.rb new file mode 100644 index 0000000000..1367dfe092 --- /dev/null +++ b/lib/rss/content/1.0.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: false +require 'rss/1.0' + +module RSS + RDF.install_ns(CONTENT_PREFIX, CONTENT_URI) + + class RDF + class Item; include ContentModel; end + end +end diff --git a/lib/rss/content/2.0.rb b/lib/rss/content/2.0.rb new file mode 100644 index 0000000000..3b468248ac --- /dev/null +++ b/lib/rss/content/2.0.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: false +require "rss/2.0" + +module RSS + Rss.install_ns(CONTENT_PREFIX, CONTENT_URI) + + class Rss + class Channel + class Item; include ContentModel; end + end + end +end diff --git a/lib/rss/converter.rb b/lib/rss/converter.rb index d928c48223..b92e35a051 100644 --- a/lib/rss/converter.rb +++ b/lib/rss/converter.rb @@ -1,12 +1,17 @@ +# frozen_string_literal: false require "rss/utils" module RSS class Converter - + include Utils def initialize(to_enc, from_enc=nil) + if "".respond_to?(:encode) + @to_encoding = to_enc + return + end normalized_to_enc = to_enc.downcase.gsub(/-/, '_') from_enc ||= 'utf-8' normalized_from_enc = from_enc.downcase.gsub(/-/, '_') @@ -23,7 +28,11 @@ module RSS end def convert(value) - value + if value.is_a?(String) and value.respond_to?(:encode) + value.encode(@to_encoding) + else + value + end end def def_convert(depth=0) @@ -55,11 +64,11 @@ module RSS raise UnknownConversionMethodError.new(to_enc, from_enc) end end - + def def_else_enc(to_enc, from_enc) def_iconv_convert(to_enc, from_enc, 0) end - + def def_same_enc() def_convert do |value| value @@ -80,8 +89,12 @@ module RSS end rescue LoadError require 'nkf' - def_convert(1) do |value| - "NKF.nkf(#{nkf_arg.dump}, #{value})" + if NKF.const_defined?(:UTF8) + def_convert(1) do |value| + "NKF.nkf(#{nkf_arg.dump}, #{value})" + end + else + def_iconv_convert(to_enc, from_enc, 1) end end end @@ -89,40 +102,40 @@ module RSS def def_to_euc_jp_from_utf_8 def_uconv_convert_if_can('u8toeuc', 'EUC-JP', 'UTF-8', '-We') end - + def def_to_utf_8_from_euc_jp def_uconv_convert_if_can('euctou8', 'UTF-8', 'EUC-JP', '-Ew') end - + def def_to_shift_jis_from_utf_8 def_uconv_convert_if_can('u8tosjis', 'Shift_JIS', 'UTF-8', '-Ws') end - + def def_to_utf_8_from_shift_jis def_uconv_convert_if_can('sjistou8', 'UTF-8', 'Shift_JIS', '-Sw') end - + def def_to_euc_jp_from_shift_jis require "nkf" def_convert do |value| "NKF.nkf('-Se', #{value})" end end - + def def_to_shift_jis_from_euc_jp require "nkf" def_convert do |value| "NKF.nkf('-Es', #{value})" end end - + def def_to_euc_jp_from_iso_2022_jp require "nkf" def_convert do |value| "NKF.nkf('-Je', #{value})" end end - + def def_to_iso_2022_jp_from_euc_jp require "nkf" def_convert do |value| @@ -135,7 +148,7 @@ module RSS "#{value}.unpack('C*').pack('U*')" end end - + def def_to_iso_8859_1_from_utf_8 def_convert do |value| <<-EOC @@ -152,7 +165,7 @@ module RSS EOC end end - + end - + end diff --git a/lib/rss/dublincore.rb b/lib/rss/dublincore.rb index 8a4afd4dd9..8d1a551947 100644 --- a/lib/rss/dublincore.rb +++ b/lib/rss/dublincore.rb @@ -1,11 +1,11 @@ -require "rss/1.0" +# frozen_string_literal: false +require "rss/rss" module RSS - + # The prefix for the Dublin Core XML namespace. DC_PREFIX = 'dc' + # The URI of the Dublin Core specification. DC_URI = "http://purl.org/dc/elements/1.1/" - - RDF.install_ns(DC_PREFIX, DC_URI) module BaseDublinCoreModel def append_features(klass) @@ -28,7 +28,7 @@ module RSS def #{full_name} @#{full_name}.first and @#{full_name}.first.value end - + def #{full_name}=(new_value) @#{full_name}[0] = Utils.new_with_value_if_need(#{klass_name}, new_value) end @@ -36,12 +36,24 @@ module RSS EOC end klass.module_eval(<<-EOC, *get_file_and_line_from_caller(0)) - alias date #{DC_PREFIX}_date - alias date= #{DC_PREFIX}_date= + if method_defined?(:date) + alias date_without_#{DC_PREFIX}_date= date= + + def date=(value) + self.date_without_#{DC_PREFIX}_date = value + self.#{DC_PREFIX}_date = value + end + else + alias date #{DC_PREFIX}_date + alias date= #{DC_PREFIX}_date= + end + + # For backward compatibility + alias #{DC_PREFIX}_rightses #{DC_PREFIX}_rights_list EOC end end - + module DublinCoreModel extend BaseModel @@ -61,32 +73,32 @@ module RSS "language" => nil, "relation" => nil, "coverage" => nil, - "rights" => "rightses" # FIXME + "rights" => "rights_list" } DATE_ELEMENTS = { "date" => "w3cdtf", } - + ELEMENT_NAME_INFOS = DublinCoreModel::TEXT_ELEMENTS.to_a DublinCoreModel::DATE_ELEMENTS.each do |name, | ELEMENT_NAME_INFOS << [name, nil] end - + ELEMENTS = TEXT_ELEMENTS.keys + DATE_ELEMENTS.keys ELEMENTS.each do |name, plural_name| module_eval(<<-EOC, *get_file_and_line_from_caller(0)) class DublinCore#{Utils.to_class_name(name)} < Element include RSS10 - + content_setup class << self def required_prefix DC_PREFIX end - + def required_uri DC_URI end @@ -96,7 +108,7 @@ module RSS alias_method(:value, :content) alias_method(:value=, :content=) - + def initialize(*args) if Utils.element_initialize_arguments?(args) super @@ -105,7 +117,7 @@ module RSS self.content = args[0] end end - + def full_name tag_name_with_prefix(DC_PREFIX) end @@ -122,13 +134,14 @@ module RSS end DATE_ELEMENTS.each do |name, type| + tag_name = "#{DC_PREFIX}:#{name}" module_eval(<<-EOC, *get_file_and_line_from_caller(0)) class DublinCore#{Utils.to_class_name(name)} < Element remove_method(:content=) remove_method(:value=) - date_writer("content", #{type.dump}, #{name.dump}) - + date_writer("content", #{type.dump}, #{tag_name.dump}) + alias_method(:value=, :content=) end EOC @@ -138,13 +151,6 @@ module RSS # For backward compatibility DublincoreModel = DublinCoreModel - class RDF - class Channel; include DublinCoreModel; end - class Image; include DublinCoreModel; end - class Item; include DublinCoreModel; end - class Textinput; include DublinCoreModel; end - end - DublinCoreModel::ELEMENTS.each do |name| class_name = Utils.to_class_name(name) BaseListener.install_class_name(DC_URI, name, "DublinCore#{class_name}") @@ -152,3 +158,7 @@ module RSS DublinCoreModel::ELEMENTS.collect! {|name| "#{DC_PREFIX}_#{name}"} end + +require 'rss/dublincore/1.0' +require 'rss/dublincore/2.0' +require 'rss/dublincore/atom' diff --git a/lib/rss/dublincore/1.0.rb b/lib/rss/dublincore/1.0.rb new file mode 100644 index 0000000000..1d96fab9b9 --- /dev/null +++ b/lib/rss/dublincore/1.0.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: false +require "rss/1.0" + +module RSS + RDF.install_ns(DC_PREFIX, DC_URI) + + class RDF + class Channel; include DublinCoreModel; end + class Image; include DublinCoreModel; end + class Item; include DublinCoreModel; end + class Textinput; include DublinCoreModel; end + end +end diff --git a/lib/rss/dublincore/2.0.rb b/lib/rss/dublincore/2.0.rb new file mode 100644 index 0000000000..e3011fef6a --- /dev/null +++ b/lib/rss/dublincore/2.0.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: false +require "rss/2.0" + +module RSS + Rss.install_ns(DC_PREFIX, DC_URI) + + class Rss + class Channel + include DublinCoreModel + class Item; include DublinCoreModel; end + end + end +end diff --git a/lib/rss/dublincore/atom.rb b/lib/rss/dublincore/atom.rb new file mode 100644 index 0000000000..0b8b11e440 --- /dev/null +++ b/lib/rss/dublincore/atom.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: false +require "rss/atom" + +module RSS + module Atom + Feed.install_ns(DC_PREFIX, DC_URI) + + class Feed + include DublinCoreModel + class Entry; include DublinCoreModel; end + end + + class Entry + include DublinCoreModel + end + end +end diff --git a/lib/rss/image.rb b/lib/rss/image.rb index a9e9e9094e..6b86ec0e5b 100644 --- a/lib/rss/image.rb +++ b/lib/rss/image.rb @@ -1,13 +1,18 @@ +# frozen_string_literal: false require 'rss/1.0' require 'rss/dublincore' module RSS + # The prefix for the Image XML namespace. IMAGE_PREFIX = 'image' - IMAGE_URI = 'http://web.resource.org/rss/1.0/modules/image/' + # The URI for the Image specification. + IMAGE_URI = 'http://purl.org/rss/1.0/modules/image/' RDF.install_ns(IMAGE_PREFIX, IMAGE_URI) + # This constant holds strings which contain the names of + # image elements, with the appropriate prefix. IMAGE_ELEMENTS = [] %w(item favicon).each do |name| @@ -15,7 +20,7 @@ module RSS BaseListener.install_class_name(IMAGE_URI, name, "Image#{class_name}") IMAGE_ELEMENTS << "#{IMAGE_PREFIX}_#{name}" end - + module ImageModelUtils def validate_one_tag_name(ignore_unknown_element, name, tags) if !ignore_unknown_element @@ -25,7 +30,7 @@ module RSS raise TooMuchTagError.new(name, tag_name) if tags.size > 1 end end - + module ImageItemModel include ImageModelUtils extend BaseModel @@ -43,12 +48,12 @@ module RSS include DublinCoreModel @tag_name = "item" - + class << self def required_prefix IMAGE_PREFIX end - + def required_uri IMAGE_URI end @@ -69,7 +74,7 @@ module RSS disp_name = "#{IMAGE_PREFIX}:#{tag}" install_text_element(tag, IMAGE_URI, "?", full_name, :integer, disp_name) - BaseListener.install_get_text_element(IMAGE_URI, tag, "#{full_name}=") + BaseListener.install_get_text_element(IMAGE_URI, tag, full_name) end alias width= image_width= @@ -102,11 +107,11 @@ module RSS end end end - + module ImageFaviconModel include ImageModelUtils extend BaseModel - + def self.append_features(klass) super @@ -122,12 +127,12 @@ module RSS include DublinCoreModel @tag_name = "favicon" - + class << self def required_prefix IMAGE_PREFIX end - + def required_uri IMAGE_URI end @@ -142,8 +147,8 @@ module RSS end AVAILABLE_SIZES = %w(small medium large) - alias_method :_size=, :size= - private :_size= + alias_method :set_size, :size= + private :set_size def size=(new_value) if @do_validate and !new_value.nil? new_value = new_value.strip @@ -152,9 +157,9 @@ module RSS raise NotAvailableValueError.new(full_name, new_value, attr_name) end end - __send__(:_size=, new_value) + set_size(new_value) end - + alias image_size= size= alias image_size size diff --git a/lib/rss/itunes.rb b/lib/rss/itunes.rb new file mode 100644 index 0000000000..987b090f21 --- /dev/null +++ b/lib/rss/itunes.rb @@ -0,0 +1,413 @@ +# frozen_string_literal: false +require 'rss/2.0' + +module RSS + # The prefix for the iTunes XML namespace. + ITUNES_PREFIX = 'itunes' + # The URI of the iTunes specification. + ITUNES_URI = 'http://www.itunes.com/dtds/podcast-1.0.dtd' + + Rss.install_ns(ITUNES_PREFIX, ITUNES_URI) + + module ITunesModelUtils + include Utils + + def def_class_accessor(klass, name, type, *args) + normalized_name = name.gsub(/-/, "_") + full_name = "#{ITUNES_PREFIX}_#{normalized_name}" + klass_name = "ITunes#{Utils.to_class_name(normalized_name)}" + + case type + when :element, :attribute + klass::ELEMENTS << full_name + def_element_class_accessor(klass, name, full_name, klass_name, *args) + when :elements + klass::ELEMENTS << full_name + def_elements_class_accessor(klass, name, full_name, klass_name, *args) + else + klass.install_must_call_validator(ITUNES_PREFIX, ITUNES_URI) + klass.install_text_element(normalized_name, ITUNES_URI, "?", + full_name, type, name) + end + end + + def def_element_class_accessor(klass, name, full_name, klass_name, + recommended_attribute_name=nil) + klass.install_have_child_element(name, ITUNES_PREFIX, "?", full_name) + end + + def def_elements_class_accessor(klass, name, full_name, klass_name, + plural_name, recommended_attribute_name=nil) + full_plural_name = "#{ITUNES_PREFIX}_#{plural_name}" + klass.install_have_children_element(name, ITUNES_PREFIX, "*", + full_name, full_plural_name) + end + end + + module ITunesBaseModel + extend ITunesModelUtils + + ELEMENTS = [] + + ELEMENT_INFOS = [["author"], + ["block", :yes_other], + ["explicit", :explicit_clean_other], + ["keywords", :csv], + ["subtitle"], + ["summary"]] + end + + module ITunesChannelModel + extend BaseModel + extend ITunesModelUtils + include ITunesBaseModel + + ELEMENTS = [] + + class << self + def append_features(klass) + super + + return if klass.instance_of?(Module) + ELEMENT_INFOS.each do |name, type, *additional_infos| + def_class_accessor(klass, name, type, *additional_infos) + end + end + end + + ELEMENT_INFOS = [ + ["category", :elements, "categories", "text"], + ["image", :attribute, "href"], + ["owner", :element], + ["new-feed-url"], + ] + ITunesBaseModel::ELEMENT_INFOS + + class ITunesCategory < Element + include RSS09 + + @tag_name = "category" + + class << self + def required_prefix + ITUNES_PREFIX + end + + def required_uri + ITUNES_URI + end + end + + [ + ["text", "", true] + ].each do |name, uri, required| + install_get_attribute(name, uri, required) + end + + ITunesCategory = self + install_have_children_element("category", ITUNES_URI, "*", + "#{ITUNES_PREFIX}_category", + "#{ITUNES_PREFIX}_categories") + + def initialize(*args) + if Utils.element_initialize_arguments?(args) + super + else + super() + self.text = args[0] + end + end + + def full_name + tag_name_with_prefix(ITUNES_PREFIX) + end + + private + def maker_target(categories) + if text or !itunes_categories.empty? + categories.new_category + else + nil + end + end + + def setup_maker_attributes(category) + category.text = text if text + end + + def setup_maker_elements(category) + super(category) + itunes_categories.each do |sub_category| + sub_category.setup_maker(category) + end + end + end + + class ITunesImage < Element + include RSS09 + + @tag_name = "image" + + class << self + def required_prefix + ITUNES_PREFIX + end + + def required_uri + ITUNES_URI + end + end + + [ + ["href", "", true] + ].each do |name, uri, required| + install_get_attribute(name, uri, required) + end + + def initialize(*args) + if Utils.element_initialize_arguments?(args) + super + else + super() + self.href = args[0] + end + end + + def full_name + tag_name_with_prefix(ITUNES_PREFIX) + end + + private + def maker_target(target) + if href + target.itunes_image {|image| image} + else + nil + end + end + + def setup_maker_attributes(image) + image.href = href + end + end + + class ITunesOwner < Element + include RSS09 + + @tag_name = "owner" + + class << self + def required_prefix + ITUNES_PREFIX + end + + def required_uri + ITUNES_URI + end + end + + install_must_call_validator(ITUNES_PREFIX, ITUNES_URI) + [ + ["name"], + ["email"], + ].each do |name,| + ITunesBaseModel::ELEMENT_INFOS << name + install_text_element(name, ITUNES_URI, nil, "#{ITUNES_PREFIX}_#{name}") + end + + def initialize(*args) + if Utils.element_initialize_arguments?(args) + super + else + super() + self.itunes_name = args[0] + self.itunes_email = args[1] + end + end + + def full_name + tag_name_with_prefix(ITUNES_PREFIX) + end + + private + def maker_target(target) + target.itunes_owner + end + + def setup_maker_element(owner) + super(owner) + owner.itunes_name = itunes_name + owner.itunes_email = itunes_email + end + end + end + + module ITunesItemModel + extend BaseModel + extend ITunesModelUtils + include ITunesBaseModel + + class << self + def append_features(klass) + super + + return if klass.instance_of?(Module) + ELEMENT_INFOS.each do |name, type| + def_class_accessor(klass, name, type) + end + end + end + + ELEMENT_INFOS = ITunesBaseModel::ELEMENT_INFOS + + [["duration", :element, "content"]] + + class ITunesDuration < Element + include RSS09 + + @tag_name = "duration" + + class << self + def required_prefix + ITUNES_PREFIX + end + + def required_uri + ITUNES_URI + end + + def parse(duration, do_validate=true) + if do_validate and /\A(?: + \d?\d:[0-5]\d:[0-5]\d| + [0-5]?\d:[0-5]\d + )\z/x !~ duration + raise ArgumentError, + "must be one of HH:MM:SS, H:MM:SS, MM::SS, M:SS: " + + duration.inspect + end + + components = duration.split(':') + components[3..-1] = nil if components.size > 3 + + components.unshift("00") until components.size == 3 + + components.collect do |component| + component.to_i + end + end + + def construct(hour, minute, second) + components = [minute, second] + if components.include?(nil) + nil + else + components.unshift(hour) if hour and hour > 0 + components.collect do |component| + "%02d" % component + end.join(":") + end + end + end + + content_setup + alias_method(:value, :content) + remove_method(:content=) + + attr_reader :hour, :minute, :second + def initialize(*args) + if Utils.element_initialize_arguments?(args) + super + else + super() + args = args[0] if args.size == 1 and args[0].is_a?(Array) + if args.size == 1 + self.content = args[0] + elsif args.size > 3 + raise ArgumentError, + "must be (do_validate, params), (content), " + + "(minute, second), ([minute, second]), " + + "(hour, minute, second) or ([hour, minute, second]): " + + args.inspect + else + @second, @minute, @hour = args.reverse + update_content + end + end + end + + def content=(value) + if value.nil? + @content = nil + elsif value.is_a?(self.class) + self.content = value.content + else + begin + @hour, @minute, @second = self.class.parse(value, @do_validate) + rescue ArgumentError + raise NotAvailableValueError.new(tag_name, value) + end + @content = value + end + end + alias_method(:value=, :content=) + + def hour=(hour) + @hour = @do_validate ? Integer(hour) : hour.to_i + update_content + hour + end + + def minute=(minute) + @minute = @do_validate ? Integer(minute) : minute.to_i + update_content + minute + end + + def second=(second) + @second = @do_validate ? Integer(second) : second.to_i + update_content + second + end + + def full_name + tag_name_with_prefix(ITUNES_PREFIX) + end + + private + def update_content + @content = self.class.construct(hour, minute, second) + end + + def maker_target(target) + if @content + target.itunes_duration {|duration| duration} + else + nil + end + end + + def setup_maker_element(duration) + super(duration) + duration.content = @content + end + end + end + + class Rss + class Channel + include ITunesChannelModel + class Item; include ITunesItemModel; end + end + end + + element_infos = + ITunesChannelModel::ELEMENT_INFOS + ITunesItemModel::ELEMENT_INFOS + element_infos.each do |name, type| + case type + when :element, :elements, :attribute + class_name = Utils.to_class_name(name) + BaseListener.install_class_name(ITUNES_URI, name, "ITunes#{class_name}") + else + accessor_base = "#{ITUNES_PREFIX}_#{name.gsub(/-/, '_')}" + BaseListener.install_get_text_element(ITUNES_URI, name, accessor_base) + end + end +end diff --git a/lib/rss/maker.rb b/lib/rss/maker.rb index 9ed799ac7f..33d285f6af 100644 --- a/lib/rss/maker.rb +++ b/lib/rss/maker.rb @@ -1,37 +1,79 @@ +# frozen_string_literal: false require "rss/rss" module RSS + ## + # + # Provides a set of builders for various RSS objects + # + # * Feeds + # * RSS 0.91 + # * RSS 1.0 + # * RSS 2.0 + # * Atom 1.0 + # + # * Elements + # * Atom::Entry module Maker + # Collection of supported makers MAKERS = {} - + class << self + # Builder for an RSS object + # Creates an object of the type passed in +args+ + # + # Executes the +block+ to populate elements of the created RSS object def make(version, &block) - maker(version).make(&block) + self[version].make(&block) end - def maker(version) - MAKERS[version] + # Returns the maker for the +version+ + def [](version) + maker_info = maker(version) + raise UnsupportedMakerVersionError.new(version) if maker_info.nil? + maker_info[:maker] end - def add_maker(version, maker) - MAKERS[version] = maker + # Adds a maker to the set of supported makers + def add_maker(version, normalized_version, maker) + MAKERS[version] = {:maker => maker, :version => normalized_version} end - def filename_to_version(filename) - File.basename(filename, ".*") + # Returns collection of supported maker versions + def versions + MAKERS.keys.uniq.sort + end + + # Returns collection of supported makers + def makers + MAKERS.values.collect { |info| info[:maker] }.uniq + end + + # Returns true if the version is supported + def supported?(version) + versions.include?(version) + end + + private + # Can I remove this method? + def maker(version) + MAKERS[version] end end end - end require "rss/maker/1.0" require "rss/maker/2.0" +require "rss/maker/feed" +require "rss/maker/entry" require "rss/maker/content" require "rss/maker/dublincore" +require "rss/maker/slash" require "rss/maker/syndication" require "rss/maker/taxonomy" require "rss/maker/trackback" require "rss/maker/image" +require "rss/maker/itunes" diff --git a/lib/rss/maker/0.9.rb b/lib/rss/maker/0.9.rb index b82585fb96..622a4c30b4 100644 --- a/lib/rss/maker/0.9.rb +++ b/lib/rss/maker/0.9.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: false require "rss/0.9" require "rss/maker/base" module RSS module Maker - + class RSS09 < RSSBase - - def initialize(rss_version="0.91") + + def initialize(feed_version) super + @feed_type = "rss" end - + private - def make_rss - Rss.new(@rss_version, @version, @encoding, @standalone) + def make_feed + Rss.new(@feed_version, @version, @encoding, @standalone) end def setup_elements(rss) @@ -21,204 +23,487 @@ module RSS end class Channel < ChannelBase - - def to_rss(rss) + def to_feed(rss) channel = Rss::Channel.new - set = setup_values(channel) - if set + setup_values(channel) + _not_set_required_variables = not_set_required_variables + if _not_set_required_variables.empty? rss.channel = channel + set_parent(channel, rss) setup_items(rss) setup_image(rss) setup_textinput(rss) - setup_other_elements(rss) - if rss.channel.image - rss - else - nil - end - elsif variable_is_set? - raise NotSetError.new("maker.channel", not_set_required_variables) + setup_other_elements(rss, channel) + rss + else + raise NotSetError.new("maker.channel", _not_set_required_variables) end end - - def have_required_values? - @title and @link and @description and @language - end - + private def setup_items(rss) - @maker.items.to_rss(rss) + @maker.items.to_feed(rss) end - + def setup_image(rss) - @maker.image.to_rss(rss) + @maker.image.to_feed(rss) end - + def setup_textinput(rss) - @maker.textinput.to_rss(rss) + @maker.textinput.to_feed(rss) end - + def variables super + ["pubDate"] end def required_variable_names - %w(title link description language) + %w(link language) + end + + def not_set_required_variables + vars = super + vars << "description" unless description {|d| d.have_required_values?} + vars << "title" unless title {|t| t.have_required_values?} + vars end - + class SkipDays < SkipDaysBase - def to_rss(rss, channel) + def to_feed(rss, channel) unless @days.empty? skipDays = Rss::Channel::SkipDays.new channel.skipDays = skipDays + set_parent(skipDays, channel) @days.each do |day| - day.to_rss(rss, skipDays.days) + day.to_feed(rss, skipDays.days) end end end - + class Day < DayBase - def to_rss(rss, days) + def to_feed(rss, days) day = Rss::Channel::SkipDays::Day.new set = setup_values(day) if set days << day - setup_other_elements(rss) + set_parent(day, days) + setup_other_elements(rss, day) end end - def have_required_values? - @content + private + def required_variable_names + %w(content) end end end - + class SkipHours < SkipHoursBase - def to_rss(rss, channel) + def to_feed(rss, channel) unless @hours.empty? skipHours = Rss::Channel::SkipHours.new channel.skipHours = skipHours + set_parent(skipHours, channel) @hours.each do |hour| - hour.to_rss(rss, skipHours.hours) + hour.to_feed(rss, skipHours.hours) end end end - + class Hour < HourBase - def to_rss(rss, hours) + def to_feed(rss, hours) hour = Rss::Channel::SkipHours::Hour.new set = setup_values(hour) if set hours << hour - setup_other_elements(rss) + set_parent(hour, hours) + setup_other_elements(rss, hour) end end - def have_required_values? - @content + private + def required_variable_names + %w(content) end end end - + class Cloud < CloudBase - def to_rss(*args) + def to_feed(*args) end end class Categories < CategoriesBase - def to_rss(*args) + def to_feed(*args) end class Category < CategoryBase end end + + class Links < LinksBase + def to_feed(rss, channel) + return if @links.empty? + @links.first.to_feed(rss, channel) + end + + class Link < LinkBase + def to_feed(rss, channel) + if have_required_values? + channel.link = href + else + raise NotSetError.new("maker.channel.link", + not_set_required_variables) + end + end + + private + def required_variable_names + %w(href) + end + end + end + + class Authors < AuthorsBase + def to_feed(rss, channel) + end + + class Author < AuthorBase + def to_feed(rss, channel) + end + end + end + + class Contributors < ContributorsBase + def to_feed(rss, channel) + end + + class Contributor < ContributorBase + end + end + + class Generator < GeneratorBase + def to_feed(rss, channel) + end + end + + class Copyright < CopyrightBase + def to_feed(rss, channel) + channel.copyright = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end + + class Description < DescriptionBase + def to_feed(rss, channel) + channel.description = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end + + class Title < TitleBase + def to_feed(rss, channel) + channel.title = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end end - + class Image < ImageBase - def to_rss(rss) + def to_feed(rss) image = Rss::Channel::Image.new set = setup_values(image) if set image.link = link rss.channel.image = image - setup_other_elements(rss) + set_parent(image, rss.channel) + setup_other_elements(rss, image) + elsif required_element? + raise NotSetError.new("maker.image", not_set_required_variables) end end - - def have_required_values? - @url and @title and link + + private + def required_variable_names + %w(url title link) + end + + def required_element? + true end end - + class Items < ItemsBase - def to_rss(rss) + def to_feed(rss) if rss.channel normalize.each do |item| - item.to_rss(rss) + item.to_feed(rss) end - setup_other_elements(rss) + setup_other_elements(rss, rss.items) end end - + class Item < ItemBase - def to_rss(rss) + def to_feed(rss) item = Rss::Channel::Item.new - set = setup_values(item) - if set + setup_values(item) + _not_set_required_variables = not_set_required_variables + if _not_set_required_variables.empty? rss.items << item - setup_other_elements(rss) + set_parent(item, rss.channel) + setup_other_elements(rss, item) + elsif variable_is_set? + raise NotSetError.new("maker.items", _not_set_required_variables) end end - + private - def have_required_values? - @title and @link + def required_variable_names + [] + end + + def not_set_required_variables + vars = super + if @maker.feed_version == "0.91" + vars << "title" unless title {|t| t.have_required_values?} + vars << "link" unless link {|l| l.have_required_values?} + end + vars end class Guid < GuidBase - def to_rss(*args) + def to_feed(*args) end end - + class Enclosure < EnclosureBase - def to_rss(*args) + def to_feed(*args) end end - + class Source < SourceBase - def to_rss(*args) + def to_feed(*args) + end + + class Authors < AuthorsBase + def to_feed(*args) + end + + class Author < AuthorBase + end + end + + class Categories < CategoriesBase + def to_feed(*args) + end + + class Category < CategoryBase + end + end + + class Contributors < ContributorsBase + def to_feed(*args) + end + + class Contributor < ContributorBase + end + end + + class Generator < GeneratorBase + def to_feed(*args) + end + end + + class Icon < IconBase + def to_feed(*args) + end + end + + class Links < LinksBase + def to_feed(*args) + end + + class Link < LinkBase + end + end + + class Logo < LogoBase + def to_feed(*args) + end + end + + class Rights < RightsBase + def to_feed(*args) + end + end + + class Subtitle < SubtitleBase + def to_feed(*args) + end + end + + class Title < TitleBase + def to_feed(*args) + end end end - + class Categories < CategoriesBase - def to_rss(*args) + def to_feed(*args) end class Category < CategoryBase end end - + + class Authors < AuthorsBase + def to_feed(*args) + end + + class Author < AuthorBase + end + end + + class Links < LinksBase + def to_feed(rss, item) + return if @links.empty? + @links.first.to_feed(rss, item) + end + + class Link < LinkBase + def to_feed(rss, item) + if have_required_values? + item.link = href + else + raise NotSetError.new("maker.link", + not_set_required_variables) + end + end + + private + def required_variable_names + %w(href) + end + end + end + + class Contributors < ContributorsBase + def to_feed(rss, item) + end + + class Contributor < ContributorBase + end + end + + class Rights < RightsBase + def to_feed(rss, item) + end + end + + class Description < DescriptionBase + def to_feed(rss, item) + item.description = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end + + class Content < ContentBase + def to_feed(rss, item) + end + end + + class Title < TitleBase + def to_feed(rss, item) + item.title = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end end end - + class Textinput < TextinputBase - def to_rss(rss) + def to_feed(rss) textInput = Rss::Channel::TextInput.new set = setup_values(textInput) if set rss.channel.textInput = textInput - setup_other_elements(rss) + set_parent(textInput, rss.channel) + setup_other_elements(rss, textInput) end end private - def have_required_values? - @title and @description and @name and @link + def required_variable_names + %w(title description name link) end end end - - add_maker(filename_to_version(__FILE__), RSS09) - add_maker(filename_to_version(__FILE__) + "1", RSS09) + + class RSS091 < RSS09 + def initialize(feed_version="0.91") + super + end + + class Channel < RSS09::Channel + end + + class Items < RSS09::Items + class Item < RSS09::Items::Item + end + end + + class Image < RSS09::Image + end + + class Textinput < RSS09::Textinput + end + end + + class RSS092 < RSS09 + def initialize(feed_version="0.92") + super + end + + class Channel < RSS09::Channel + end + + class Items < RSS09::Items + class Item < RSS09::Items::Item + end + end + + class Image < RSS09::Image + end + + class Textinput < RSS09::Textinput + end + end + + add_maker("0.9", "0.92", RSS092) + add_maker("0.91", "0.91", RSS091) + add_maker("0.92", "0.92", RSS092) + add_maker("rss0.9", "0.92", RSS092) + add_maker("rss0.91", "0.91", RSS091) + add_maker("rss0.92", "0.92", RSS092) end end diff --git a/lib/rss/maker/1.0.rb b/lib/rss/maker/1.0.rb index 3e6542a007..3aee77e913 100644 --- a/lib/rss/maker/1.0.rb +++ b/lib/rss/maker/1.0.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require "rss/1.0" require "rss/maker/base" @@ -7,12 +8,13 @@ module RSS class RSS10 < RSSBase - def initialize - super("1.0") + def initialize(feed_version="1.0") + super + @feed_type = "rss" end private - def make_rss + def make_feed RDF.new(@version, @encoding, @standalone) end @@ -24,44 +26,48 @@ module RSS end class Channel < ChannelBase + include SetupDefaultLanguage - def to_rss(rss) - set = false - if @about - channel = RDF::Channel.new(@about) - set = setup_values(channel) - if set + def to_feed(rss) + set_default_values do + _not_set_required_variables = not_set_required_variables + if _not_set_required_variables.empty? + channel = RDF::Channel.new(@about) + setup_values(channel) channel.dc_dates.clear rss.channel = channel + set_parent(channel, rss) setup_items(rss) setup_image(rss) setup_textinput(rss) - setup_other_elements(rss) + setup_other_elements(rss, channel) + else + raise NotSetError.new("maker.channel", _not_set_required_variables) end end - - if (!@about or !set) and variable_is_set? - raise NotSetError.new("maker.channel", not_set_required_variables) - end - end - - def have_required_values? - @about and @title and @link and @description end private def setup_items(rss) items = RDF::Channel::Items.new seq = items.Seq - @maker.items.normalize.each do |item| - seq.lis << RDF::Channel::Items::Seq::Li.new(item.link) + set_parent(items, seq) + target_items = @maker.items.normalize + raise NotSetError.new("maker", ["items"]) if target_items.empty? + target_items.each do |item| + li = RDF::Channel::Items::Seq::Li.new(item.link) + seq.lis << li + set_parent(li, seq) end rss.channel.items = items + set_parent(rss.channel, items) end - + def setup_image(rss) if @maker.image.have_required_values? - rss.channel.image = RDF::Channel::Image.new(@maker.image.url) + image = RDF::Channel::Image.new(@maker.image.url) + rss.channel.image = image + set_parent(image, rss.channel) end end @@ -69,136 +75,362 @@ module RSS if @maker.textinput.have_required_values? textinput = RDF::Channel::Textinput.new(@maker.textinput.link) rss.channel.textinput = textinput + set_parent(textinput, rss.channel) end end def required_variable_names - %w(about title link description) + %w(about link) + end + + def not_set_required_variables + vars = super + vars << "description" unless description {|d| d.have_required_values?} + vars << "title" unless title {|t| t.have_required_values?} + vars end - + class SkipDays < SkipDaysBase - def to_rss(*args) + def to_feed(*args) end - + class Day < DayBase end end - + class SkipHours < SkipHoursBase - def to_rss(*args) + def to_feed(*args) end class Hour < HourBase end end - + class Cloud < CloudBase - def to_rss(*args) + def to_feed(*args) end end class Categories < CategoriesBase - def to_rss(*args) + def to_feed(*args) end class Category < CategoryBase end end + + class Links < LinksBase + def to_feed(rss, channel) + return if @links.empty? + @links.first.to_feed(rss, channel) + end + + class Link < LinkBase + def to_feed(rss, channel) + if have_required_values? + channel.link = href + else + raise NotSetError.new("maker.channel.link", + not_set_required_variables) + end + end + + private + def required_variable_names + %w(href) + end + end + end + + class Authors < AuthorsBase + def to_feed(rss, channel) + end + + class Author < AuthorBase + def to_feed(rss, channel) + end + end + end + + class Contributors < ContributorsBase + def to_feed(rss, channel) + end + + class Contributor < ContributorBase + end + end + + class Generator < GeneratorBase + def to_feed(rss, channel) + end + end + + class Copyright < CopyrightBase + def to_feed(rss, channel) + end + end + + class Description < DescriptionBase + def to_feed(rss, channel) + channel.description = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end + + class Title < TitleBase + def to_feed(rss, channel) + channel.title = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end end class Image < ImageBase - def to_rss(rss) + def to_feed(rss) if @url image = RDF::Image.new(@url) set = setup_values(image) if set rss.image = image - setup_other_elements(rss) + set_parent(image, rss) + setup_other_elements(rss, image) end end end def have_required_values? - @url and @title and link and @maker.channel.have_required_values? + super and @maker.channel.have_required_values? end private def variables super + ["link"] end + + def required_variable_names + %w(url title link) + end end class Items < ItemsBase - def to_rss(rss) + def to_feed(rss) if rss.channel normalize.each do |item| - item.to_rss(rss) + item.to_feed(rss) end - setup_other_elements(rss) + setup_other_elements(rss, rss.items) end end class Item < ItemBase - def to_rss(rss) - if @link - item = RDF::Item.new(@link) + def to_feed(rss) + set_default_values do + item = RDF::Item.new(link) set = setup_values(item) if set item.dc_dates.clear rss.items << item - setup_other_elements(rss) + set_parent(item, rss) + setup_other_elements(rss, item) + elsif !have_required_values? + raise NotSetError.new("maker.item", not_set_required_variables) end end end - def have_required_values? - @title and @link + private + def required_variable_names + %w(link) + end + + def variables + super + %w(link) + end + + def not_set_required_variables + set_default_values do + vars = super + vars << "title" unless title {|t| t.have_required_values?} + vars + end end class Guid < GuidBase - def to_rss(*args) + def to_feed(*args) end end - + class Enclosure < EnclosureBase - def to_rss(*args) + def to_feed(*args) end end - + class Source < SourceBase - def to_rss(*args) + def to_feed(*args) + end + + class Authors < AuthorsBase + def to_feed(*args) + end + + class Author < AuthorBase + end + end + + class Categories < CategoriesBase + def to_feed(*args) + end + + class Category < CategoryBase + end + end + + class Contributors < ContributorsBase + def to_feed(*args) + end + + class Contributor < ContributorBase + end + end + + class Generator < GeneratorBase + def to_feed(*args) + end + end + + class Icon < IconBase + def to_feed(*args) + end + end + + class Links < LinksBase + def to_feed(*args) + end + + class Link < LinkBase + end + end + + class Logo < LogoBase + def to_feed(*args) + end + end + + class Rights < RightsBase + def to_feed(*args) + end + end + + class Subtitle < SubtitleBase + def to_feed(*args) + end + end + + class Title < TitleBase + def to_feed(*args) + end end end - + class Categories < CategoriesBase - def to_rss(*args) + def to_feed(*args) end class Category < CategoryBase end end + + class Authors < AuthorsBase + def to_feed(*args) + end + + class Author < AuthorBase + end + end + + class Links < LinksBase + def to_feed(*args) + end + + class Link < LinkBase + end + end + + class Contributors < ContributorsBase + def to_feed(rss, item) + end + + class Contributor < ContributorBase + end + end + + class Rights < RightsBase + def to_feed(rss, item) + end + end + + class Description < DescriptionBase + def to_feed(rss, item) + item.description = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end + + class Content < ContentBase + def to_feed(rss, item) + end + end + + class Title < TitleBase + def to_feed(rss, item) + item.title = content if have_required_values? + end + + private + def required_variable_names + %w(content) + end + end end end - + class Textinput < TextinputBase - def to_rss(rss) + def to_feed(rss) if @link textinput = RDF::Textinput.new(@link) set = setup_values(textinput) if set rss.textinput = textinput - setup_other_elements(rss) + set_parent(textinput, rss) + setup_other_elements(rss, textinput) end end end def have_required_values? - @title and @description and @name and @link and - @maker.channel.have_required_values? + super and @maker.channel.have_required_values? + end + + private + def required_variable_names + %w(title description name link) end end end - add_maker(filename_to_version(__FILE__), RSS10) + add_maker("1.0", "1.0", RSS10) + add_maker("rss1.0", "1.0", RSS10) end end diff --git a/lib/rss/maker/2.0.rb b/lib/rss/maker/2.0.rb index a958661614..1f77a014d1 100644 --- a/lib/rss/maker/2.0.rb +++ b/lib/rss/maker/2.0.rb @@ -1,168 +1,224 @@ +# frozen_string_literal: false require "rss/2.0" require "rss/maker/0.9" module RSS module Maker - + class RSS20 < RSS09 - - def initialize(rss_version="2.0") + + def initialize(feed_version="2.0") super end class Channel < RSS09::Channel - def have_required_values? - @title and @link and @description - end - + private def required_variable_names - %w(title link description) + %w(link) end - + class SkipDays < RSS09::Channel::SkipDays class Day < RSS09::Channel::SkipDays::Day end end - + class SkipHours < RSS09::Channel::SkipHours class Hour < RSS09::Channel::SkipHours::Hour end end - + class Cloud < RSS09::Channel::Cloud - def to_rss(rss, channel) + def to_feed(rss, channel) cloud = Rss::Channel::Cloud.new set = setup_values(cloud) if set channel.cloud = cloud - setup_other_elements(rss) + set_parent(cloud, channel) + setup_other_elements(rss, cloud) end end - def have_required_values? - @domain and @port and @path and - @registerProcedure and @protocol + private + def required_variable_names + %w(domain port path registerProcedure protocol) end end class Categories < RSS09::Channel::Categories - def to_rss(rss, channel) + def to_feed(rss, channel) @categories.each do |category| - category.to_rss(rss, channel) + category.to_feed(rss, channel) end end - + class Category < RSS09::Channel::Categories::Category - def to_rss(rss, channel) + def to_feed(rss, channel) category = Rss::Channel::Category.new set = setup_values(category) if set channel.categories << category - setup_other_elements(rss) + set_parent(category, channel) + setup_other_elements(rss, category) end end - - def have_required_values? - @content + + private + def required_variable_names + %w(content) end end end - + + class Generator < GeneratorBase + def to_feed(rss, channel) + channel.generator = content + end + + private + def required_variable_names + %w(content) + end + end end - + class Image < RSS09::Image + private + def required_element? + false + end end - + class Items < RSS09::Items - class Item < RSS09::Items::Item + private + def required_variable_names + [] + end - def have_required_values? - @title or @description + def not_set_required_variables + vars = super + if !title {|t| t.have_required_values?} and + !description {|d| d.have_required_values?} + vars << "title or description" + end + vars end - private def variables super + ["pubDate"] end class Guid < RSS09::Items::Item::Guid - def to_rss(rss, item) + def to_feed(rss, item) guid = Rss::Channel::Item::Guid.new set = setup_values(guid) if set item.guid = guid - setup_other_elements(rss) + set_parent(guid, item) + setup_other_elements(rss, guid) end end - - def have_required_values? - @content + + private + def required_variable_names + %w(content) end end class Enclosure < RSS09::Items::Item::Enclosure - def to_rss(rss, item) + def to_feed(rss, item) enclosure = Rss::Channel::Item::Enclosure.new set = setup_values(enclosure) if set item.enclosure = enclosure - setup_other_elements(rss) + set_parent(enclosure, item) + setup_other_elements(rss, enclosure) end end - - def have_required_values? - @url and @length and @type + + private + def required_variable_names + %w(url length type) end end class Source < RSS09::Items::Item::Source - def to_rss(rss, item) + def to_feed(rss, item) source = Rss::Channel::Item::Source.new set = setup_values(source) if set item.source = source - setup_other_elements(rss) + set_parent(source, item) + setup_other_elements(rss, source) end end - - def have_required_values? - @url and @content + + private + def required_variable_names + %w(url content) + end + + class Links < RSS09::Items::Item::Source::Links + def to_feed(rss, source) + return if @links.empty? + @links.first.to_feed(rss, source) + end + + class Link < RSS09::Items::Item::Source::Links::Link + def to_feed(rss, source) + source.url = href + end + end end end class Categories < RSS09::Items::Item::Categories - def to_rss(rss, item) + def to_feed(rss, item) @categories.each do |category| - category.to_rss(rss, item) + category.to_feed(rss, item) end end - + class Category < RSS09::Items::Item::Categories::Category - def to_rss(rss, item) + def to_feed(rss, item) category = Rss::Channel::Item::Category.new set = setup_values(category) if set item.categories << category + set_parent(category, item) setup_other_elements(rss) end end - - def have_required_values? - @content + + private + def required_variable_names + %w(content) + end + end + end + + class Authors < RSS09::Items::Item::Authors + def to_feed(rss, item) + return if @authors.empty? + @authors.first.to_feed(rss, item) + end + + class Author < RSS09::Items::Item::Authors::Author + def to_feed(rss, item) + item.author = name end end end end - end - + class Textinput < RSS09::Textinput end end - - add_maker(filename_to_version(__FILE__), RSS20) + + add_maker("2.0", "2.0", RSS20) + add_maker("rss2.0", "2.0", RSS20) end end diff --git a/lib/rss/maker/atom.rb b/lib/rss/maker/atom.rb new file mode 100644 index 0000000000..e0cd7623c8 --- /dev/null +++ b/lib/rss/maker/atom.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: false +require "rss/atom" + +require "rss/maker/base" + +module RSS + module Maker + module AtomPersons + module_function + def def_atom_persons(klass, name, maker_name, plural=nil) + plural ||= "#{name}s" + klass_name = Utils.to_class_name(name) + plural_klass_name = Utils.to_class_name(plural) + + klass.class_eval(<<-EOC, __FILE__, __LINE__ + 1) + class #{plural_klass_name} < #{plural_klass_name}Base + class #{klass_name} < #{klass_name}Base + def to_feed(feed, current) + #{name} = feed.class::#{klass_name}.new + set = setup_values(#{name}) + unless set + raise NotSetError.new(#{maker_name.dump}, + not_set_required_variables) + end + current.#{plural} << #{name} + set_parent(#{name}, current) + setup_other_elements(#{name}) + end + + private + def required_variable_names + %w(name) + end + end + end +EOC + end + end + + module AtomTextConstruct + class << self + def def_atom_text_construct(klass, name, maker_name, klass_name=nil, + atom_klass_name=nil) + klass_name ||= Utils.to_class_name(name) + atom_klass_name ||= Utils.to_class_name(name) + + klass.class_eval(<<-EOC, __FILE__, __LINE__ + 1) + class #{klass_name} < #{klass_name}Base + include #{self.name} + def to_feed(feed, current) + #{name} = current.class::#{atom_klass_name}.new + if setup_values(#{name}) + current.#{name} = #{name} + set_parent(#{name}, current) + setup_other_elements(feed) + elsif variable_is_set? + raise NotSetError.new(#{maker_name.dump}, + not_set_required_variables) + end + end + end + EOC + end + end + + private + def required_variable_names + if type == "xhtml" + %w(xml_content) + else + %w(content) + end + end + + def variables + if type == "xhtml" + super + %w(xhtml) + else + super + end + end + end + + module AtomCategory + def to_feed(feed, current) + category = feed.class::Category.new + set = setup_values(category) + if set + current.categories << category + set_parent(category, current) + setup_other_elements(feed) + else + raise NotSetError.new(self.class.not_set_name, + not_set_required_variables) + end + end + + private + def required_variable_names + %w(term) + end + + def variables + super + ["term", "scheme"] + end + end + + module AtomLink + def to_feed(feed, current) + link = feed.class::Link.new + set = setup_values(link) + if set + current.links << link + set_parent(link, current) + setup_other_elements(feed) + else + raise NotSetError.new(self.class.not_set_name, + not_set_required_variables) + end + end + + private + def required_variable_names + %w(href) + end + end + + module AtomGenerator + def to_feed(feed, current) + generator = current.class::Generator.new + if setup_values(generator) + current.generator = generator + set_parent(generator, current) + setup_other_elements(feed) + elsif variable_is_set? + raise NotSetError.new(self.class.not_set_name, + not_set_required_variables) + end + end + + private + def required_variable_names + %w(content) + end + end + + module AtomLogo + def to_feed(feed, current) + logo = current.class::Logo.new + class << logo + alias_method(:uri=, :content=) + end + set = setup_values(logo) + class << logo + remove_method(:uri=) + end + if set + current.logo = logo + set_parent(logo, current) + setup_other_elements(feed) + elsif variable_is_set? + raise NotSetError.new(self.class.not_set_name, + not_set_required_variables) + end + end + + private + def required_variable_names + %w(uri) + end + end + end +end diff --git a/lib/rss/maker/base.rb b/lib/rss/maker/base.rb index 2327dd98e4..bc4ca84141 100644 --- a/lib/rss/maker/base.rb +++ b/lib/rss/maker/base.rb @@ -1,90 +1,237 @@ +# frozen_string_literal: false require 'forwardable' require 'rss/rss' module RSS module Maker + class Base + extend Utils::InheritedReader - module Base + OTHER_ELEMENTS = [] + NEED_INITIALIZE_VARIABLES = [] - def self.append_features(klass) - super + class << self + def other_elements + inherited_array_reader("OTHER_ELEMENTS") + end + def need_initialize_variables + inherited_array_reader("NEED_INITIALIZE_VARIABLES") + end - klass.module_eval(<<-EOC, __FILE__, __LINE__) + def inherited_base + ::RSS::Maker::Base + end - OTHER_ELEMENTS = [] - NEED_INITIALIZE_VARIABLES = [] + def inherited(subclass) + subclass.const_set(:OTHER_ELEMENTS, []) + subclass.const_set(:NEED_INITIALIZE_VARIABLES, []) + end - def self.inherited(subclass) - subclass.const_set("OTHER_ELEMENTS", []) - subclass.const_set("NEED_INITIALIZE_VARIABLES", []) + def add_other_element(variable_name) + self::OTHER_ELEMENTS << variable_name + end + + def add_need_initialize_variable(variable_name, init_value=nil, + &init_block) + init_value ||= init_block + self::NEED_INITIALIZE_VARIABLES << [variable_name, init_value] + end - subclass.module_eval(<<-EOEOC, __FILE__, __LINE__) - def self.other_elements - OTHER_ELEMENTS + super + def def_array_element(name, plural=nil, klass_name=nil) + include Enumerable + extend Forwardable + + plural ||= "#{name}s" + klass_name ||= Utils.to_class_name(name) + def_delegators("@#{plural}", :<<, :[], :[]=, :first, :last) + def_delegators("@#{plural}", :push, :pop, :shift, :unshift) + def_delegators("@#{plural}", :each, :size, :empty?, :clear) + + add_need_initialize_variable(plural) {[]} + + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def new_#{name} + #{name} = self.class::#{klass_name}.new(@maker) + @#{plural} << #{name} + if block_given? + yield #{name} + else + #{name} + end end + alias new_child new_#{name} - def self.need_initialize_variables - NEED_INITIALIZE_VARIABLES + super + def to_feed(*args) + @#{plural}.each do |#{name}| + #{name}.to_feed(*args) + end end - EOEOC - end - def self.add_other_element(variable_name) - OTHER_ELEMENTS << variable_name + def replace(elements) + @#{plural}.replace(elements.to_a) + end + EOC end - def self.other_elements - OTHER_ELEMENTS + def def_classed_element_without_accessor(name, class_name=nil) + class_name ||= Utils.to_class_name(name) + add_other_element(name) + add_need_initialize_variable(name) do |object| + object.send("make_#{name}") + end + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + private + def setup_#{name}(feed, current) + @#{name}.to_feed(feed, current) + end + + def make_#{name} + self.class::#{class_name}.new(@maker) + end + EOC + end + + def def_classed_element(name, class_name=nil, attribute_name=nil) + def_classed_element_without_accessor(name, class_name) + if attribute_name + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{name} + if block_given? + yield(@#{name}) + else + @#{name}.#{attribute_name} + end + end + + def #{name}=(new_value) + @#{name}.#{attribute_name} = new_value + end + EOC + else + attr_reader name + end end - def self.add_need_initialize_variable(variable_name, init_value="nil") - NEED_INITIALIZE_VARIABLES << [variable_name, init_value] + def def_classed_elements(name, attribute, plural_class_name=nil, + plural_name=nil, new_name=nil) + plural_name ||= "#{name}s" + new_name ||= name + def_classed_element(plural_name, plural_class_name) + local_variable_name = "_#{name}" + new_value_variable_name = "new_value" + additional_setup_code = nil + if block_given? + additional_setup_code = yield(local_variable_name, + new_value_variable_name) + end + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{name} + #{local_variable_name} = #{plural_name}.first + #{local_variable_name} ? #{local_variable_name}.#{attribute} : nil + end + + def #{name}=(#{new_value_variable_name}) + #{local_variable_name} = + #{plural_name}.first || #{plural_name}.new_#{new_name} + #{additional_setup_code} + #{local_variable_name}.#{attribute} = #{new_value_variable_name} + end + EOC end - def self.need_initialize_variables - NEED_INITIALIZE_VARIABLES + def def_other_element(name) + attr_accessor name + def_other_element_without_accessor(name) end - def self.def_array_element(name) - include Enumerable - extend Forwardable + def def_other_element_without_accessor(name) + add_need_initialize_variable(name) + add_other_element(name) + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def setup_#{name}(feed, current) + if !@#{name}.nil? and current.respond_to?(:#{name}=) + current.#{name} = @#{name} + end + end + EOC + end - def_delegators("@\#{name}", :<<, :[], :[]=, :first, :last) - def_delegators("@\#{name}", :push, :pop, :shift, :unshift) - def_delegators("@\#{name}", :each, :size) - - add_need_initialize_variable(name, "[]") + def def_csv_element(name, type=nil) + def_other_element_without_accessor(name) + attr_reader(name) + converter = "" + if type == :integer + converter = "{|v| Integer(v)}" + end + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{name}=(value) + @#{name} = Utils::CSV.parse(value)#{converter} + end + EOC end - EOC end - + + attr_reader :maker def initialize(maker) @maker = maker + @default_values_are_set = false initialize_variables end def have_required_values? - true + not_set_required_variables.empty? + end + + def variable_is_set? + variables.any? {|var| not __send__(var).nil?} end - + private def initialize_variables self.class.need_initialize_variables.each do |variable_name, init_value| - instance_eval("@#{variable_name} = #{init_value}", __FILE__, __LINE__) + if init_value.nil? + value = nil + else + if init_value.respond_to?(:call) + value = init_value.call(self) + elsif init_value.is_a?(String) + # just for backward compatibility + value = instance_eval(init_value, __FILE__, __LINE__) + else + value = init_value + end + end + instance_variable_set("@#{variable_name}", value) end end - def setup_other_elements(rss) + def setup_other_elements(feed, current=nil) + current ||= current_element(feed) self.class.other_elements.each do |element| - __send__("setup_#{element}", rss, current_element(rss)) + __send__("setup_#{element}", feed, current) end end - def current_element(rss) - rss + def current_element(feed) + feed end - + + def set_default_values(&block) + return yield if @default_values_are_set + + begin + @default_values_are_set = true + _set_default_values(&block) + ensure + @default_values_are_set = false + end + end + + def _set_default_values(&block) + yield + end + def setup_values(target) set = false if have_required_values? @@ -92,7 +239,7 @@ module RSS setter = "#{var}=" if target.respond_to?(setter) value = __send__(var) - if value + unless value.nil? target.__send__(setter, value) set = true end @@ -102,18 +249,19 @@ module RSS set end + def set_parent(target, parent) + target.parent = parent if target.class.need_parent? + end + def variables self.class.need_initialize_variables.find_all do |name, init| - "nil" == init + # init == "nil" is just for backward compatibility + init.nil? or init == "nil" end.collect do |name, init| name end end - def variable_is_set? - variables.find {|var| !__send__(var).nil?} - end - def not_set_required_variables required_variable_names.find_all do |var| __send__(var).nil? @@ -126,115 +274,207 @@ module RSS end true end - end - class RSSBase - include Base + module AtomPersonConstructBase + def self.append_features(klass) + super + + klass.class_eval(<<-EOC, __FILE__, __LINE__ + 1) + %w(name uri email).each do |element| + attr_accessor element + add_need_initialize_variable(element) + end + EOC + end + end + + module AtomTextConstructBase + module EnsureXMLContent + class << self + def included(base) + super + base.class_eval do + %w(type content xml_content).each do |element| + attr_reader element + attr_writer element if element != "xml_content" + add_need_initialize_variable(element) + end + + alias_method(:xhtml, :xml_content) + end + end + end + + def ensure_xml_content(content) + xhtml_uri = ::RSS::Atom::XHTML_URI + unless content.is_a?(RSS::XML::Element) and + ["div", xhtml_uri] == [content.name, content.uri] + children = content + children = [children] unless content.is_a?(Array) + children = set_xhtml_uri_as_default_uri(children) + content = RSS::XML::Element.new("div", nil, xhtml_uri, + {"xmlns" => xhtml_uri}, + children) + end + content + end + + def xml_content=(content) + @xml_content = ensure_xml_content(content) + end + + def xhtml=(content) + self.xml_content = content + end + + private + def set_xhtml_uri_as_default_uri(children) + children.collect do |child| + if child.is_a?(RSS::XML::Element) and + child.prefix.nil? and child.uri.nil? + RSS::XML::Element.new(child.name, nil, ::RSS::Atom::XHTML_URI, + child.attributes.dup, + set_xhtml_uri_as_default_uri(child.children)) + else + child + end + end + end + end + + def self.append_features(klass) + super + + klass.class_eval do + include EnsureXMLContent + end + end + end + + module SetupDefaultDate + private + def _set_default_values + keep = { + :date => date, + :dc_dates => dc_dates.to_a.dup, + } + _date = _parse_date_if_needed(date) + if _date and !dc_dates.any? {|dc_date| dc_date.value == _date} + dc_date = self.class::DublinCoreDates::DublinCoreDate.new(self) + dc_date.value = _date.dup + dc_dates.unshift(dc_date) + end + self.date ||= self.dc_date + super + ensure + self.date = keep[:date] + dc_dates.replace(keep[:dc_dates]) + end + + def _parse_date_if_needed(date_value) + date_value = Time.parse(date_value) if date_value.is_a?(String) + date_value + end + end + module SetupDefaultLanguage + private + def _set_default_values + keep = { + :dc_languages => dc_languages.to_a.dup, + } + _language = language + if _language and + !dc_languages.any? {|dc_language| dc_language.value == _language} + dc_language = self.class::DublinCoreLanguages::DublinCoreLanguage.new(self) + dc_language.value = _language.dup + dc_languages.unshift(dc_language) + end + super + ensure + dc_languages.replace(keep[:dc_languages]) + end + end + + class RSSBase < Base class << self - def make(&block) - new.make(&block) + def make(*args, &block) + new(*args).make(&block) end end %w(xml_stylesheets channel image items textinput).each do |element| attr_reader element - add_need_initialize_variable(element, "make_#{element}") - module_eval(<<-EOC, __FILE__, __LINE__) + add_need_initialize_variable(element) do |object| + object.send("make_#{element}") + end + module_eval(<<-EOC, __FILE__, __LINE__ + 1) private - def setup_#{element}(rss) - @#{element}.to_rss(rss) + def setup_#{element}(feed) + @#{element}.to_feed(feed) end def make_#{element} self.class::#{Utils.to_class_name(element)}.new(self) end -EOC + EOC end - - attr_reader :rss_version + + attr_reader :feed_version + alias_method(:rss_version, :feed_version) attr_accessor :version, :encoding, :standalone - - def initialize(rss_version) + + def initialize(feed_version) super(self) - @rss_version = rss_version + @feed_type = nil + @feed_subtype = nil + @feed_version = feed_version @version = "1.0" @encoding = "UTF-8" @standalone = nil end - + def make - if block_given? - yield(self) - to_rss - else - nil - end + yield(self) + to_feed end - def to_rss - rss = make_rss - setup_xml_stylesheets(rss) - setup_elements(rss) - setup_other_elements(rss) - if rss.channel - rss - else - nil - end + def to_feed + feed = make_feed + setup_xml_stylesheets(feed) + setup_elements(feed) + setup_other_elements(feed) + feed.validate + feed end - + private remove_method :make_xml_stylesheets def make_xml_stylesheets XMLStyleSheets.new(self) end - end - class XMLStyleSheets - include Base - - def_array_element("xml_stylesheets") + class XMLStyleSheets < Base + def_array_element("xml_stylesheet", nil, "XMLStyleSheet") - def to_rss(rss) - @xml_stylesheets.each do |xss| - xss.to_rss(rss) - end - end - - def new_xml_stylesheet - xss = XMLStyleSheet.new(@maker) - @xml_stylesheets << xss - if block_given? - yield xss - else - xss - end - end - - class XMLStyleSheet - include Base + class XMLStyleSheet < Base ::RSS::XMLStyleSheet::ATTRIBUTES.each do |attribute| attr_accessor attribute add_need_initialize_variable(attribute) end - - def to_rss(rss) + + def to_feed(feed) xss = ::RSS::XMLStyleSheet.new guess_type_if_need(xss) set = setup_values(xss) if set - rss.xml_stylesheets << xss + feed.xml_stylesheets << xss end end - def have_required_values? - @href and @type - end - private def guess_type_if_need(xss) if @type.nil? @@ -242,180 +482,203 @@ EOC @type = xss.type end end + + def required_variable_names + %w(href type) + end end end - - class ChannelBase - include Base - %w(cloud categories skipDays skipHours).each do |element| - attr_reader element - add_other_element(element) - add_need_initialize_variable(element, "make_#{element}") - module_eval(<<-EOC, __FILE__, __LINE__) - private - def setup_#{element}(rss, current) - @#{element}.to_rss(rss, current) - end + class ChannelBase < Base + include SetupDefaultDate - def make_#{element} - self.class::#{Utils.to_class_name(element)}.new(@maker) - end -EOC + %w(cloud categories skipDays skipHours).each do |name| + def_classed_element(name) + end + + %w(generator copyright description title).each do |name| + def_classed_element(name, nil, "content") end - %w(about title link description language copyright - managingEditor webMaster rating docs date - lastBuildDate generator ttl).each do |element| + [ + ["link", "href", Proc.new {|target,| "#{target}.href = 'self'"}], + ["author", "name"], + ["contributor", "name"], + ].each do |name, attribute, additional_setup_maker| + def_classed_elements(name, attribute, &additional_setup_maker) + end + + %w(id about language + managingEditor webMaster rating docs ttl).each do |element| attr_accessor element add_need_initialize_variable(element) end - alias_method(:pubDate, :date) - alias_method(:pubDate=, :date=) + %w(date lastBuildDate).each do |date_element| + attr_reader date_element + add_need_initialize_variable(date_element) + end - def current_element(rss) - rss.channel + def date=(_date) + @date = _parse_date_if_needed(_date) end - class SkipDaysBase - include Base + def lastBuildDate=(_date) + @lastBuildDate = _parse_date_if_needed(_date) + end - def_array_element("days") + def pubDate + date + end - def new_day - day = self.class::Day.new(@maker) - @days << day - if block_given? - yield day - else - day - end - end - - def current_element(rss) - rss.channel.skipDays - end + def pubDate=(date) + self.date = date + end - class DayBase - include Base - - %w(content).each do |element| - attr_accessor element - add_need_initialize_variable(element) - end + def updated + date + end - def current_element(rss) - rss.channel.skipDays.last - end + def updated=(date) + self.date = date + end - end + alias_method(:rights, :copyright) + alias_method(:rights=, :copyright=) + + alias_method(:subtitle, :description) + alias_method(:subtitle=, :description=) + + def icon + image_favicon.about end - - class SkipHoursBase - include Base - def_array_element("hours") + def icon=(url) + image_favicon.about = url + end - def new_hour - hour = self.class::Hour.new(@maker) - @hours << hour - if block_given? - yield hour - else - hour - end - end - - def current_element(rss) - rss.channel.skipHours - end + def logo + maker.image.url + end - class HourBase - include Base - + def logo=(url) + maker.image.url = url + end + + class SkipDaysBase < Base + def_array_element("day") + + class DayBase < Base %w(content).each do |element| attr_accessor element add_need_initialize_variable(element) end + end + end - def current_element(rss) - rss.channel.skipHours.last - end + class SkipHoursBase < Base + def_array_element("hour") + class HourBase < Base + %w(content).each do |element| + attr_accessor element + add_need_initialize_variable(element) + end end end - - class CloudBase - include Base - + + class CloudBase < Base %w(domain port path registerProcedure protocol).each do |element| attr_accessor element add_need_initialize_variable(element) end - - def current_element(rss) - rss.channel.cloud - end - end - class CategoriesBase - include Base - - def_array_element("categories") + class CategoriesBase < Base + def_array_element("category", "categories") - def new_category - category = self.class::Category.new(@maker) - @categories << category - if block_given? - yield category - else - category + class CategoryBase < Base + %w(domain content label).each do |element| + attr_accessor element + add_need_initialize_variable(element) end + + alias_method(:term, :domain) + alias_method(:term=, :domain=) + alias_method(:scheme, :content) + alias_method(:scheme=, :content=) end + end - class CategoryBase - include Base + class LinksBase < Base + def_array_element("link") - %w(domain content).each do |element| + class LinkBase < Base + %w(href rel type hreflang title length).each do |element| attr_accessor element add_need_initialize_variable(element) end end end + + class AuthorsBase < Base + def_array_element("author") + + class AuthorBase < Base + include AtomPersonConstructBase + end + end + + class ContributorsBase < Base + def_array_element("contributor") + + class ContributorBase < Base + include AtomPersonConstructBase + end + end + + class GeneratorBase < Base + %w(uri version content).each do |element| + attr_accessor element + add_need_initialize_variable(element) + end + end + + class CopyrightBase < Base + include AtomTextConstructBase + end + + class DescriptionBase < Base + include AtomTextConstructBase + end + + class TitleBase < Base + include AtomTextConstructBase + end end - - class ImageBase - include Base + class ImageBase < Base %w(title url width height description).each do |element| attr_accessor element add_need_initialize_variable(element) end - + def link @maker.channel.link end - - def current_element(rss) - rss.image - end end - - class ItemsBase - include Base - def_array_element("items") - + class ItemsBase < Base + def_array_element("item") + attr_accessor :do_sort, :max_size - + def initialize(maker) super @do_sort = false @max_size = -1 end - + def normalize if @max_size >= 0 sort_if_need[0...@max_size] @@ -423,21 +686,7 @@ EOC sort_if_need[0..@max_size] end end - - def current_element(rss) - rss.items - end - def new_item - item = self.class::Item.new(@maker) - @items << item - if block_given? - yield item - else - item - end - end - private def sort_if_need if @do_sort.respond_to?(:call) @@ -453,94 +702,244 @@ EOC end end - class ItemBase - include Base - - %w(guid enclosure source categories).each do |element| - attr_reader element - add_other_element(element) - add_need_initialize_variable(element, "make_#{element}") - module_eval(<<-EOC, __FILE__, __LINE__) - private - def setup_#{element}(rss, current) - @#{element}.to_rss(rss, current) - end + class ItemBase < Base + include SetupDefaultDate - def make_#{element} - self.class::#{Utils.to_class_name(element)}.new(@maker) - end -EOC + %w(guid enclosure source categories content).each do |name| + def_classed_element(name) end - - %w(title link description date author comments).each do |element| + + %w(rights description title).each do |name| + def_classed_element(name, nil, "content") + end + + [ + ["author", "name"], + ["link", "href", Proc.new {|target,| "#{target}.href = 'alternate'"}], + ["contributor", "name"], + ].each do |name, attribute| + def_classed_elements(name, attribute) + end + + %w(comments id published).each do |element| attr_accessor element add_need_initialize_variable(element) end - alias_method(:pubDate, :date) - alias_method(:pubDate=, :date=) + %w(date).each do |date_element| + attr_reader date_element + add_need_initialize_variable(date_element) + end + + def date=(_date) + @date = _parse_date_if_needed(_date) + end + + def pubDate + date + end + + def pubDate=(date) + self.date = date + end + + def updated + date + end + + def updated=(date) + self.date = date + end + + alias_method(:summary, :description) + alias_method(:summary=, :description=) def <=>(other) - if date and other.date - date <=> other.date - elsif date + _date = date || dc_date + _other_date = other.date || other.dc_date + if _date and _other_date + _date <=> _other_date + elsif _date 1 - elsif other.date + elsif _other_date -1 else 0 end end - - def current_element(rss) - rss.items.last - end - - class GuidBase - include Base + class GuidBase < Base %w(isPermaLink content).each do |element| attr_accessor element add_need_initialize_variable(element) end + + def permanent_link? + isPermaLink + end + + def permanent_link=(bool) + self.isPermaLink = bool + end end - - class EnclosureBase - include Base + class EnclosureBase < Base %w(url length type).each do |element| attr_accessor element add_need_initialize_variable(element) end end - - class SourceBase - include Base - %w(url content).each do |element| + class SourceBase < Base + include SetupDefaultDate + + %w(authors categories contributors generator icon + logo rights subtitle title).each do |name| + def_classed_element(name) + end + + [ + ["link", "href"], + ].each do |name, attribute| + def_classed_elements(name, attribute) + end + + %w(id content).each do |element| attr_accessor element add_need_initialize_variable(element) end + + alias_method(:url, :link) + alias_method(:url=, :link=) + + %w(date).each do |date_element| + attr_reader date_element + add_need_initialize_variable(date_element) + end + + def date=(_date) + @date = _parse_date_if_needed(_date) + end + + def updated + date + end + + def updated=(date) + self.date = date + end + + private + AuthorsBase = ChannelBase::AuthorsBase + CategoriesBase = ChannelBase::CategoriesBase + ContributorsBase = ChannelBase::ContributorsBase + GeneratorBase = ChannelBase::GeneratorBase + + class IconBase < Base + %w(url).each do |element| + attr_accessor element + add_need_initialize_variable(element) + end + end + + LinksBase = ChannelBase::LinksBase + + class LogoBase < Base + %w(uri).each do |element| + attr_accessor element + add_need_initialize_variable(element) + end + end + + class RightsBase < Base + include AtomTextConstructBase + end + + class SubtitleBase < Base + include AtomTextConstructBase + end + + class TitleBase < Base + include AtomTextConstructBase + end end - + CategoriesBase = ChannelBase::CategoriesBase - + AuthorsBase = ChannelBase::AuthorsBase + LinksBase = ChannelBase::LinksBase + ContributorsBase = ChannelBase::ContributorsBase + + class RightsBase < Base + include AtomTextConstructBase + end + + class DescriptionBase < Base + include AtomTextConstructBase + end + + class ContentBase < Base + include AtomTextConstructBase::EnsureXMLContent + + %w(src).each do |element| + attr_accessor(element) + add_need_initialize_variable(element) + end + + def xml_content=(content) + content = ensure_xml_content(content) if inline_xhtml? + @xml_content = content + end + + alias_method(:xml, :xml_content) + alias_method(:xml=, :xml_content=) + + def inline_text? + [nil, "text", "html"].include?(@type) + end + + def inline_html? + @type == "html" + end + + def inline_xhtml? + @type == "xhtml" + end + + def inline_other? + !out_of_line? and ![nil, "text", "html", "xhtml"].include?(@type) + end + + def inline_other_text? + return false if @type.nil? or out_of_line? + /\Atext\//i.match(@type) ? true : false + end + + def inline_other_xml? + return false if @type.nil? or out_of_line? + /[\+\/]xml\z/i.match(@type) ? true : false + end + + def inline_other_base64? + return false if @type.nil? or out_of_line? + @type.include?("/") and !inline_other_text? and !inline_other_xml? + end + + def out_of_line? + not @src.nil? and @content.nil? + end + end + + class TitleBase < Base + include AtomTextConstructBase + end end end - class TextinputBase - include Base - + class TextinputBase < Base %w(title description name link).each do |element| attr_accessor element add_need_initialize_variable(element) end - - def current_element(rss) - rss.textinput - end - end - end end diff --git a/lib/rss/maker/content.rb b/lib/rss/maker/content.rb index 18590d0cf8..3559a45ad0 100644 --- a/lib/rss/maker/content.rb +++ b/lib/rss/maker/content.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: false require 'rss/content' require 'rss/maker/1.0' +require 'rss/maker/2.0' module RSS module Maker @@ -7,17 +9,8 @@ module RSS def self.append_features(klass) super - ::RSS::ContentModel::ELEMENTS.each do |element| - klass.add_need_initialize_variable(element) - klass.add_other_element(element) - klass.module_eval(<<-EOC, __FILE__, __LINE__+1) - attr_accessor :#{element} - def setup_#{element}(rss, current) - if #{element} and current.respond_to?(:#{element}=) - current.#{element} = @#{element} if @#{element} - end - end - EOC + ::RSS::ContentModel::ELEMENTS.each do |name| + klass.def_other_element(name) end end end diff --git a/lib/rss/maker/dublincore.rb b/lib/rss/maker/dublincore.rb index 0cf1255e82..988209c045 100644 --- a/lib/rss/maker/dublincore.rb +++ b/lib/rss/maker/dublincore.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require 'rss/dublincore' require 'rss/maker/1.0' @@ -11,64 +12,41 @@ module RSS plural_name ||= "#{name}s" full_name = "#{RSS::DC_PREFIX}_#{name}" full_plural_name = "#{RSS::DC_PREFIX}_#{plural_name}" - klass_name = Utils.to_class_name(name) plural_klass_name = "DublinCore#{Utils.to_class_name(plural_name)}" - full_plural_klass_name = "self.class::#{plural_klass_name}" - full_klass_name = "#{full_plural_klass_name}::#{klass_name}" - klass.add_need_initialize_variable(full_plural_name, - "make_#{full_plural_name}") - klass.add_other_element(full_plural_name) - klass.module_eval(<<-EOC, __FILE__, __LINE__+1) - attr_accessor :#{full_plural_name} - def make_#{full_plural_name} - #{full_plural_klass_name}.new(@maker) - end - - def setup_#{full_plural_name}(rss, current) - @#{full_plural_name}.to_rss(rss, current) - end - - def #{full_name} - @#{full_plural_name}[0] and @#{full_plural_name}[0].value - end - - def #{full_name}=(new_value) - @#{full_plural_name}[0] = #{full_klass_name}.new(self) - @#{full_plural_name}[0].value = new_value + klass.def_classed_elements(full_name, "value", plural_klass_name, + full_plural_name, name) + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def new_#{full_name}(value=nil) + _#{full_name} = #{full_plural_name}.new_#{name} + _#{full_name}.value = value + if block_given? + yield _#{full_name} + else + _#{full_name} + end end -EOC + EOC end + + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + # For backward compatibility + alias #{DC_PREFIX}_rightses #{DC_PREFIX}_rights_list + EOC end ::RSS::DublinCoreModel::ELEMENT_NAME_INFOS.each do |name, plural_name| plural_name ||= "#{name}s" + full_name ||= "#{DC_PREFIX}_#{name}" + full_plural_name ||= "#{DC_PREFIX}_#{plural_name}" klass_name = Utils.to_class_name(name) + full_klass_name = "DublinCore#{klass_name}" plural_klass_name = "DublinCore#{Utils.to_class_name(plural_name)}" - module_eval(<<-EOC, __FILE__, __LINE__) - class #{plural_klass_name}Base - include Base - - def_array_element(#{plural_name.dump}) - - def new_#{name} - #{name} = self.class::#{klass_name}.new(self) - @#{plural_name} << #{name} - if block_given? - yield #{name} - else - #{name} - end - end - - def to_rss(rss, current) - @#{plural_name}.each do |#{name}| - #{name}.to_rss(rss, current) - end - end - - class #{klass_name}Base - include Base + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + class #{plural_klass_name}Base < Base + def_array_element(#{name.dump}, #{full_plural_name.dump}, + #{full_klass_name.dump}) + class #{full_klass_name}Base < Base attr_accessor :value add_need_initialize_variable("value") alias_method(:content, :value) @@ -77,7 +55,15 @@ EOC def have_required_values? @value end + + def to_feed(feed, current) + if value and current.respond_to?(:#{full_name}) + new_item = current.class::#{full_klass_name}.new(value) + current.#{full_plural_name} << new_item + end + end end + #{klass_name}Base = #{full_klass_name}Base end EOC end @@ -86,18 +72,13 @@ EOC ::RSS::DublinCoreModel::ELEMENT_NAME_INFOS.each do |name, plural_name| plural_name ||= "#{name}s" klass_name = Utils.to_class_name(name) - plural_klass_name = "DublinCore#{Utils.to_class_name(plural_name)}" full_klass_name = "DublinCore#{klass_name}" - klass.module_eval(<<-EOC, *Utils.get_file_and_line_from_caller(1)) + plural_klass_name = "DublinCore#{Utils.to_class_name(plural_name)}" + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) class #{plural_klass_name} < #{plural_klass_name}Base - class #{klass_name} < #{klass_name}Base - def to_rss(rss, current) - if value and current.respond_to?(:dc_#{name}) - new_item = current.class::#{full_klass_name}.new(value) - current.dc_#{plural_name} << new_item - end - end + class #{full_klass_name} < #{full_klass_name}Base end + #{klass_name} = #{full_klass_name} end EOC end @@ -106,64 +87,36 @@ EOC class ChannelBase include DublinCoreModel - - remove_method(:date) - remove_method(:date=) - alias_method(:date, :dc_date) - alias_method(:date=, :dc_date=) end - + class ImageBase; include DublinCoreModel; end class ItemsBase class ItemBase include DublinCoreModel - - remove_method(:date) - remove_method(:date=) - alias_method(:date, :dc_date) - alias_method(:date=, :dc_date=) end end class TextinputBase; include DublinCoreModel; end - class RSS10 - class Channel - DublinCoreModel.install_dublin_core(self) - end - - class Image - DublinCoreModel.install_dublin_core(self) - end - - class Items - class Item + makers.each do |maker| + maker.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + class Channel DublinCoreModel.install_dublin_core(self) end - end - class Textinput - DublinCoreModel.install_dublin_core(self) - end - end - - class RSS09 - class Channel - DublinCoreModel.install_dublin_core(self) - end + class Image + DublinCoreModel.install_dublin_core(self) + end - class Image - DublinCoreModel.install_dublin_core(self) - end + class Items + class Item + DublinCoreModel.install_dublin_core(self) + end + end - class Items - class Item + class Textinput DublinCoreModel.install_dublin_core(self) end - end - - class Textinput - DublinCoreModel.install_dublin_core(self) - end + EOC end end end diff --git a/lib/rss/maker/entry.rb b/lib/rss/maker/entry.rb new file mode 100644 index 0000000000..f806cbcaae --- /dev/null +++ b/lib/rss/maker/entry.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: false +require "rss/maker/atom" +require "rss/maker/feed" + +module RSS + module Maker + module Atom + class Entry < RSSBase + def initialize(feed_version="1.0") + super + @feed_type = "atom" + @feed_subtype = "entry" + end + + private + def make_feed + ::RSS::Atom::Entry.new(@version, @encoding, @standalone) + end + + def setup_elements(entry) + setup_items(entry) + end + + class Channel < ChannelBase + class SkipDays < SkipDaysBase + class Day < DayBase + end + end + + class SkipHours < SkipHoursBase + class Hour < HourBase + end + end + + class Cloud < CloudBase + end + + Categories = Feed::Channel::Categories + Links = Feed::Channel::Links + Authors = Feed::Channel::Authors + Contributors = Feed::Channel::Contributors + + class Generator < GeneratorBase + include AtomGenerator + + def self.not_set_name + "maker.channel.generator" + end + end + + Copyright = Feed::Channel::Copyright + + class Description < DescriptionBase + end + + Title = Feed::Channel::Title + end + + class Image < ImageBase + end + + class Items < ItemsBase + def to_feed(entry) + (normalize.first || Item.new(@maker)).to_feed(entry) + end + + class Item < ItemBase + def to_feed(entry) + set_default_values do + setup_values(entry) + entry.dc_dates.clear + setup_other_elements(entry) + unless have_required_values? + raise NotSetError.new("maker.item", not_set_required_variables) + end + end + end + + private + def required_variable_names + %w(id updated) + end + + def variables + super + ["updated"] + end + + def variable_is_set? + super or !authors.empty? + end + + def not_set_required_variables + set_default_values do + vars = super + if authors.all? {|author| !author.have_required_values?} + vars << "author" + end + vars << "title" unless title {|t| t.have_required_values?} + vars + end + end + + def _set_default_values + keep = { + :authors => authors.to_a.dup, + :contributors => contributors.to_a.dup, + :categories => categories.to_a.dup, + :id => id, + :links => links.to_a.dup, + :rights => @rights, + :title => @title, + :updated => updated, + } + authors.replace(@maker.channel.authors) if keep[:authors].empty? + if keep[:contributors].empty? + contributors.replace(@maker.channel.contributors) + end + if keep[:categories].empty? + categories.replace(@maker.channel.categories) + end + self.id ||= link || @maker.channel.id + links.replace(@maker.channel.links) if keep[:links].empty? + unless keep[:rights].variable_is_set? + @maker.channel.rights {|r| @rights = r} + end + unless keep[:title].variable_is_set? + @maker.channel.title {|t| @title = t} + end + self.updated ||= @maker.channel.updated + super + ensure + authors.replace(keep[:authors]) + contributors.replace(keep[:contributors]) + categories.replace(keep[:categories]) + links.replace(keep[:links]) + self.id = keep[:id] + @rights = keep[:rights] + @title = keep[:title] + self.updated = keep[:updated] + end + + Guid = Feed::Items::Item::Guid + Enclosure = Feed::Items::Item::Enclosure + Source = Feed::Items::Item::Source + Categories = Feed::Items::Item::Categories + Authors = Feed::Items::Item::Authors + Contributors = Feed::Items::Item::Contributors + Links = Feed::Items::Item::Links + Rights = Feed::Items::Item::Rights + Description = Feed::Items::Item::Description + Title = Feed::Items::Item::Title + Content = Feed::Items::Item::Content + end + end + + class Textinput < TextinputBase + end + end + end + + add_maker("atom:entry", "1.0", Atom::Entry) + add_maker("atom1.0:entry", "1.0", Atom::Entry) + end +end diff --git a/lib/rss/maker/feed.rb b/lib/rss/maker/feed.rb new file mode 100644 index 0000000000..fdef7ad643 --- /dev/null +++ b/lib/rss/maker/feed.rb @@ -0,0 +1,427 @@ +# frozen_string_literal: false +require "rss/maker/atom" + +module RSS + module Maker + module Atom + class Feed < RSSBase + def initialize(feed_version="1.0") + super + @feed_type = "atom" + @feed_subtype = "feed" + end + + private + def make_feed + ::RSS::Atom::Feed.new(@version, @encoding, @standalone) + end + + def setup_elements(feed) + setup_channel(feed) + setup_image(feed) + setup_items(feed) + end + + class Channel < ChannelBase + include SetupDefaultLanguage + + def to_feed(feed) + set_default_values do + setup_values(feed) + feed.dc_dates.clear + setup_other_elements(feed) + if image_favicon.about + icon = feed.class::Icon.new + icon.content = image_favicon.about + feed.icon = icon + end + unless have_required_values? + raise NotSetError.new("maker.channel", + not_set_required_variables) + end + end + end + + def have_required_values? + super and + (!authors.empty? or + @maker.items.any? {|item| !item.authors.empty?}) + end + + private + def required_variable_names + %w(id updated) + end + + def variables + super + %w(id updated) + end + + def variable_is_set? + super or !authors.empty? + end + + def not_set_required_variables + vars = super + if authors.empty? and + @maker.items.all? {|item| item.author.to_s.empty?} + vars << "author" + end + vars << "title" unless title {|t| t.have_required_values?} + vars + end + + def _set_default_values(&block) + keep = { + :id => id, + } + self.id ||= about + super(&block) + ensure + self.id = keep[:id] + end + + class SkipDays < SkipDaysBase + def to_feed(*args) + end + + class Day < DayBase + end + end + + class SkipHours < SkipHoursBase + def to_feed(*args) + end + + class Hour < HourBase + end + end + + class Cloud < CloudBase + def to_feed(*args) + end + end + + class Categories < CategoriesBase + class Category < CategoryBase + include AtomCategory + + def self.not_set_name + "maker.channel.category" + end + end + end + + class Links < LinksBase + class Link < LinkBase + include AtomLink + + def self.not_set_name + "maker.channel.link" + end + end + end + + AtomPersons.def_atom_persons(self, "author", "maker.channel.author") + AtomPersons.def_atom_persons(self, "contributor", + "maker.channel.contributor") + + class Generator < GeneratorBase + include AtomGenerator + + def self.not_set_name + "maker.channel.generator" + end + end + + AtomTextConstruct.def_atom_text_construct(self, "rights", + "maker.channel.copyright", + "Copyright") + AtomTextConstruct.def_atom_text_construct(self, "subtitle", + "maker.channel.description", + "Description") + AtomTextConstruct.def_atom_text_construct(self, "title", + "maker.channel.title") + end + + class Image < ImageBase + def to_feed(feed) + logo = feed.class::Logo.new + class << logo + alias_method(:url=, :content=) + end + set = setup_values(logo) + class << logo + remove_method(:url=) + end + if set + feed.logo = logo + set_parent(logo, feed) + setup_other_elements(feed, logo) + elsif variable_is_set? + raise NotSetError.new("maker.image", not_set_required_variables) + end + end + + private + def required_variable_names + %w(url) + end + end + + class Items < ItemsBase + def to_feed(feed) + normalize.each do |item| + item.to_feed(feed) + end + setup_other_elements(feed, feed.entries) + end + + class Item < ItemBase + def to_feed(feed) + set_default_values do + entry = feed.class::Entry.new + set = setup_values(entry) + entry.dc_dates.clear + setup_other_elements(feed, entry) + if set + feed.entries << entry + set_parent(entry, feed) + elsif variable_is_set? + raise NotSetError.new("maker.item", not_set_required_variables) + end + end + end + + def have_required_values? + set_default_values do + super and title {|t| t.have_required_values?} + end + end + + private + def required_variable_names + %w(id updated) + end + + def variables + super + ["updated"] + end + + def not_set_required_variables + vars = super + vars << "title" unless title {|t| t.have_required_values?} + vars + end + + def _set_default_values(&block) + keep = { + :id => id, + } + self.id ||= link + super(&block) + ensure + self.id = keep[:id] + end + + class Guid < GuidBase + def to_feed(feed, current) + end + end + + class Enclosure < EnclosureBase + def to_feed(feed, current) + end + end + + class Source < SourceBase + def to_feed(feed, current) + source = current.class::Source.new + setup_values(source) + current.source = source + set_parent(source, current) + setup_other_elements(feed, source) + current.source = nil if source.to_s == "<source/>" + end + + private + def required_variable_names + [] + end + + def variables + super + ["updated"] + end + + AtomPersons.def_atom_persons(self, "author", + "maker.item.source.author") + AtomPersons.def_atom_persons(self, "contributor", + "maker.item.source.contributor") + + class Categories < CategoriesBase + class Category < CategoryBase + include AtomCategory + + def self.not_set_name + "maker.item.source.category" + end + end + end + + class Generator < GeneratorBase + include AtomGenerator + + def self.not_set_name + "maker.item.source.generator" + end + end + + class Icon < IconBase + def to_feed(feed, current) + icon = current.class::Icon.new + class << icon + alias_method(:url=, :content=) + end + set = setup_values(icon) + class << icon + remove_method(:url=) + end + if set + current.icon = icon + set_parent(icon, current) + setup_other_elements(feed, icon) + elsif variable_is_set? + raise NotSetError.new("maker.item.source.icon", + not_set_required_variables) + end + end + + private + def required_variable_names + %w(url) + end + end + + class Links < LinksBase + class Link < LinkBase + include AtomLink + + def self.not_set_name + "maker.item.source.link" + end + end + end + + class Logo < LogoBase + include AtomLogo + + def self.not_set_name + "maker.item.source.logo" + end + end + + maker_name_base = "maker.item.source." + maker_name = "#{maker_name_base}rights" + AtomTextConstruct.def_atom_text_construct(self, "rights", + maker_name) + maker_name = "#{maker_name_base}subtitle" + AtomTextConstruct.def_atom_text_construct(self, "subtitle", + maker_name) + maker_name = "#{maker_name_base}title" + AtomTextConstruct.def_atom_text_construct(self, "title", + maker_name) + end + + class Categories < CategoriesBase + class Category < CategoryBase + include AtomCategory + + def self.not_set_name + "maker.item.category" + end + end + end + + AtomPersons.def_atom_persons(self, "author", "maker.item.author") + AtomPersons.def_atom_persons(self, "contributor", + "maker.item.contributor") + + class Links < LinksBase + class Link < LinkBase + include AtomLink + + def self.not_set_name + "maker.item.link" + end + end + end + + AtomTextConstruct.def_atom_text_construct(self, "rights", + "maker.item.rights") + AtomTextConstruct.def_atom_text_construct(self, "summary", + "maker.item.description", + "Description") + AtomTextConstruct.def_atom_text_construct(self, "title", + "maker.item.title") + + class Content < ContentBase + def to_feed(feed, current) + content = current.class::Content.new + if setup_values(content) + content.src = nil if content.src and content.content + current.content = content + set_parent(content, current) + setup_other_elements(feed, content) + elsif variable_is_set? + raise NotSetError.new("maker.item.content", + not_set_required_variables) + end + end + + alias_method(:xml, :xml_content) + + private + def required_variable_names + if out_of_line? + %w(type) + elsif xml_type? + %w(xml_content) + else + %w(content) + end + end + + def variables + if out_of_line? + super + elsif xml_type? + super + %w(xml) + else + super + end + end + + def xml_type? + _type = type + return false if _type.nil? + _type == "xhtml" or + /(?:\+xml|\/xml)$/i =~ _type or + %w(text/xml-external-parsed-entity + application/xml-external-parsed-entity + application/xml-dtd).include?(_type.downcase) + end + end + end + end + + class Textinput < TextinputBase + end + end + end + + add_maker("atom", "1.0", Atom::Feed) + add_maker("atom:feed", "1.0", Atom::Feed) + add_maker("atom1.0", "1.0", Atom::Feed) + add_maker("atom1.0:feed", "1.0", Atom::Feed) + end +end diff --git a/lib/rss/maker/image.rb b/lib/rss/maker/image.rb index ed51c8ecba..1957ba8689 100644 --- a/lib/rss/maker/image.rb +++ b/lib/rss/maker/image.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require 'rss/image' require 'rss/maker/1.0' require 'rss/maker/dublincore' @@ -9,24 +10,18 @@ module RSS super name = "#{RSS::IMAGE_PREFIX}_item" - klass.add_need_initialize_variable(name, "make_#{name}") - klass.add_other_element(name) - klass.module_eval(<<-EOC, __FILE__, __LINE__+1) - attr_reader :#{name} - def setup_#{name}(rss, current) - if @#{name} - @#{name}.to_rss(rss, current) - end - end + klass.def_classed_element(name) + end - def make_#{name} - self.class::#{Utils.to_class_name(name)}.new(@maker) + def self.install_image_item(klass) + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + class ImageItem < ImageItemBase + DublinCoreModel.install_dublin_core(self) end EOC end - class ImageItemBase - include Base + class ImageItemBase < Base include Maker::DublinCoreModel attr_accessor :about, :resource, :image_width, :image_height @@ -42,6 +37,15 @@ EOC def have_required_values? @about end + + def to_feed(feed, current) + if current.respond_to?(:image_item=) and have_required_values? + item = current.class::ImageItem.new + setup_values(item) + setup_other_elements(item) + current.image_item = item + end + end end end @@ -50,24 +54,18 @@ EOC super name = "#{RSS::IMAGE_PREFIX}_favicon" - klass.add_need_initialize_variable(name, "make_#{name}") - klass.add_other_element(name) - klass.module_eval(<<-EOC, __FILE__, __LINE__+1) - attr_reader :#{name} - def setup_#{name}(rss, current) - if @#{name} - @#{name}.to_rss(rss, current) - end - end + klass.def_classed_element(name) + end - def make_#{name} - self.class::#{Utils.to_class_name(name)}.new(@maker) + def self.install_image_favicon(klass) + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + class ImageFavicon < ImageFaviconBase + DublinCoreModel.install_dublin_core(self) end -EOC + EOC end - class ImageFaviconBase - include Base + class ImageFaviconBase < Base include Maker::DublinCoreModel attr_accessor :about, :image_size @@ -79,67 +77,36 @@ EOC def have_required_values? @about and @image_size end + + def to_feed(feed, current) + if current.respond_to?(:image_favicon=) and have_required_values? + favicon = current.class::ImageFavicon.new + setup_values(favicon) + setup_other_elements(favicon) + current.image_favicon = favicon + end + end end end class ChannelBase; include Maker::ImageFaviconModel; end - + class ItemsBase class ItemBase; include Maker::ImageItemModel; end end - class RSS10 - class Items - class Item - class ImageItem < ImageItemBase - DublinCoreModel.install_dublin_core(self) - def to_rss(rss, current) - if @about - item = ::RSS::ImageItemModel::ImageItem.new(@about, @resource) - setup_values(item) - setup_other_elements(item) - current.image_item = item - end - end - end - end - end - - class Channel - class ImageFavicon < ImageFaviconBase - DublinCoreModel.install_dublin_core(self) - def to_rss(rss, current) - if @about and @image_size - args = [@about, @image_size] - favicon = ::RSS::ImageFaviconModel::ImageFavicon.new(*args) - setup_values(favicon) - setup_other_elements(favicon) - current.image_favicon = favicon - end - end + makers.each do |maker| + maker.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + class Channel + ImageFaviconModel.install_image_favicon(self) end - end - end - class RSS09 - class Items - class Item - class ImageItem < ImageItemBase - DublinCoreModel.install_dublin_core(self) - def to_rss(*args) - end + class Items + class Item + ImageItemModel.install_image_item(self) end end - end - - class Channel - class ImageFavicon < ImageFaviconBase - DublinCoreModel.install_dublin_core(self) - def to_rss(*args) - end - end - end + EOC end - end end diff --git a/lib/rss/maker/itunes.rb b/lib/rss/maker/itunes.rb new file mode 100644 index 0000000000..d964a4d942 --- /dev/null +++ b/lib/rss/maker/itunes.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: false +require 'rss/itunes' +require 'rss/maker/2.0' + +module RSS + module Maker + module ITunesBaseModel + def def_class_accessor(klass, name, type, *args) + name = name.gsub(/-/, "_").gsub(/^itunes_/, '') + full_name = "#{RSS::ITUNES_PREFIX}_#{name}" + case type + when nil + klass.def_other_element(full_name) + when :yes_other + def_yes_other_accessor(klass, full_name) + when :explicit_clean_other + def_explicit_clean_other_accessor(klass, full_name) + when :csv + def_csv_accessor(klass, full_name) + when :element, :attribute + recommended_attribute_name, = *args + klass_name = "ITunes#{Utils.to_class_name(name)}" + klass.def_classed_element(full_name, klass_name, + recommended_attribute_name) + when :elements + plural_name, recommended_attribute_name = args + plural_name ||= "#{name}s" + full_plural_name = "#{RSS::ITUNES_PREFIX}_#{plural_name}" + klass_name = "ITunes#{Utils.to_class_name(name)}" + plural_klass_name = "ITunes#{Utils.to_class_name(plural_name)}" + def_elements_class_accessor(klass, name, full_name, full_plural_name, + klass_name, plural_klass_name, + recommended_attribute_name) + end + end + + def def_yes_other_accessor(klass, full_name) + klass.def_other_element(full_name) + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{full_name}? + Utils::YesOther.parse(@#{full_name}) + end + EOC + end + + def def_explicit_clean_other_accessor(klass, full_name) + klass.def_other_element(full_name) + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{full_name}? + Utils::ExplicitCleanOther.parse(#{full_name}) + end + EOC + end + + def def_csv_accessor(klass, full_name) + klass.def_csv_element(full_name) + end + + def def_elements_class_accessor(klass, name, full_name, full_plural_name, + klass_name, plural_klass_name, + recommended_attribute_name=nil) + if recommended_attribute_name + klass.def_classed_elements(full_name, recommended_attribute_name, + plural_klass_name, full_plural_name) + else + klass.def_classed_element(full_plural_name, plural_klass_name) + end + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def new_#{full_name}(text=nil) + #{full_name} = @#{full_plural_name}.new_#{name} + #{full_name}.text = text + if block_given? + yield #{full_name} + else + #{full_name} + end + end + EOC + end + end + + module ITunesChannelModel + extend ITunesBaseModel + + class << self + def append_features(klass) + super + + ::RSS::ITunesChannelModel::ELEMENT_INFOS.each do |name, type, *args| + def_class_accessor(klass, name, type, *args) + end + end + end + + class ITunesCategoriesBase < Base + def_array_element("category", "itunes_categories", + "ITunesCategory") + class ITunesCategoryBase < Base + attr_accessor :text + add_need_initialize_variable("text") + def_array_element("category", "itunes_categories", + "ITunesCategory") + + def have_required_values? + text + end + + alias_method :to_feed_for_categories, :to_feed + def to_feed(feed, current) + if text and current.respond_to?(:itunes_category) + new_item = current.class::ITunesCategory.new(text) + to_feed_for_categories(feed, new_item) + current.itunes_categories << new_item + end + end + end + end + + class ITunesImageBase < Base + add_need_initialize_variable("href") + attr_accessor("href") + + def to_feed(feed, current) + if @href and current.respond_to?(:itunes_image) + current.itunes_image ||= current.class::ITunesImage.new + current.itunes_image.href = @href + end + end + end + + class ITunesOwnerBase < Base + %w(itunes_name itunes_email).each do |name| + add_need_initialize_variable(name) + attr_accessor(name) + end + + def to_feed(feed, current) + if current.respond_to?(:itunes_owner=) + _not_set_required_variables = not_set_required_variables + if (required_variable_names - _not_set_required_variables).empty? + return + end + + unless have_required_values? + raise NotSetError.new("maker.channel.itunes_owner", + _not_set_required_variables) + end + current.itunes_owner ||= current.class::ITunesOwner.new + current.itunes_owner.itunes_name = @itunes_name + current.itunes_owner.itunes_email = @itunes_email + end + end + + private + def required_variable_names + %w(itunes_name itunes_email) + end + end + end + + module ITunesItemModel + extend ITunesBaseModel + + class << self + def append_features(klass) + super + + ::RSS::ITunesItemModel::ELEMENT_INFOS.each do |name, type, *args| + def_class_accessor(klass, name, type, *args) + end + end + end + + class ITunesDurationBase < Base + attr_reader :content + add_need_initialize_variable("content") + + %w(hour minute second).each do |name| + attr_reader(name) + add_need_initialize_variable(name, 0) + end + + def content=(content) + if content.nil? + @hour, @minute, @second, @content = nil + else + @hour, @minute, @second = + ::RSS::ITunesItemModel::ITunesDuration.parse(content) + @content = content + end + end + + def hour=(hour) + @hour = Integer(hour) + update_content + end + + def minute=(minute) + @minute = Integer(minute) + update_content + end + + def second=(second) + @second = Integer(second) + update_content + end + + def to_feed(feed, current) + if @content and current.respond_to?(:itunes_duration=) + current.itunes_duration ||= current.class::ITunesDuration.new + current.itunes_duration.content = @content + end + end + + private + def update_content + components = [@hour, @minute, @second] + @content = + ::RSS::ITunesItemModel::ITunesDuration.construct(*components) + end + end + end + + class ChannelBase + include Maker::ITunesChannelModel + class ITunesCategories < ITunesCategoriesBase + class ITunesCategory < ITunesCategoryBase + ITunesCategory = self + end + end + + class ITunesImage < ITunesImageBase; end + class ITunesOwner < ITunesOwnerBase; end + end + + class ItemsBase + class ItemBase + include Maker::ITunesItemModel + class ITunesDuration < ITunesDurationBase; end + end + end + end +end diff --git a/lib/rss/maker/slash.rb b/lib/rss/maker/slash.rb new file mode 100644 index 0000000000..3bd82d3057 --- /dev/null +++ b/lib/rss/maker/slash.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: false +require 'rss/slash' +require 'rss/maker/1.0' + +module RSS + module Maker + module SlashModel + def self.append_features(klass) + super + + ::RSS::SlashModel::ELEMENT_INFOS.each do |name, type| + full_name = "#{RSS::SLASH_PREFIX}_#{name}" + case type + when :csv_integer + klass.def_csv_element(full_name, :integer) + else + klass.def_other_element(full_name) + end + end + + klass.module_eval do + alias_method(:slash_hit_parades, :slash_hit_parade) + alias_method(:slash_hit_parades=, :slash_hit_parade=) + end + end + end + + class ItemsBase + class ItemBase + include SlashModel + end + end + end +end diff --git a/lib/rss/maker/syndication.rb b/lib/rss/maker/syndication.rb index 3717086257..840b70229a 100644 --- a/lib/rss/maker/syndication.rb +++ b/lib/rss/maker/syndication.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require 'rss/syndication' require 'rss/maker/1.0' @@ -7,17 +8,8 @@ module RSS def self.append_features(klass) super - ::RSS::SyndicationModel::ELEMENTS.each do |element| - klass.add_need_initialize_variable(element) - klass.add_other_element(element) - klass.module_eval(<<-EOC, __FILE__, __LINE__+1) - attr_accessor :#{element} - def setup_#{element}(rss, current) - if #{element} and current.respond_to?(:#{element}=) - current.#{element} = @#{element} if @#{element} - end - end - EOC + ::RSS::SyndicationModel::ELEMENTS.each do |name| + klass.def_other_element(name) end end end diff --git a/lib/rss/maker/taxonomy.rb b/lib/rss/maker/taxonomy.rb index f272996581..76a2d1600d 100644 --- a/lib/rss/maker/taxonomy.rb +++ b/lib/rss/maker/taxonomy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require 'rss/taxonomy' require 'rss/maker/1.0' require 'rss/maker/dublincore' @@ -8,24 +9,14 @@ module RSS def self.append_features(klass) super - klass.add_need_initialize_variable("taxo_topics", "make_taxo_topics") - klass.add_other_element("taxo_topics") - klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) - attr_reader :taxo_topics - def make_taxo_topics - self.class::TaxonomyTopics.new(@maker) - end - - def setup_taxo_topics(rss, current) - @taxo_topics.to_rss(rss, current) - end -EOC + klass.def_classed_element("#{RSS::TAXO_PREFIX}_topics", + "TaxonomyTopics") end def self.install_taxo_topics(klass) - klass.module_eval(<<-EOC, *Utils.get_file_and_line_from_caller(1)) + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) class TaxonomyTopics < TaxonomyTopicsBase - def to_rss(rss, current) + def to_feed(feed, current) if current.respond_to?(:taxo_topics) topics = current.class::TaxonomyTopics.new bag = topics.Bag @@ -39,11 +30,10 @@ EOC EOC end - class TaxonomyTopicsBase - include Base - + class TaxonomyTopicsBase < Base attr_reader :resources - def_array_element("resources") + def_array_element("resource") + remove_method :new_resource end end @@ -51,85 +41,44 @@ EOC def self.append_features(klass) super - klass.add_need_initialize_variable("taxo_topics", "make_taxo_topics") - klass.add_other_element("taxo_topics") - klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) - attr_reader :taxo_topics - def make_taxo_topics - self.class::TaxonomyTopics.new(@maker) - end - - def setup_taxo_topics(rss, current) - @taxo_topics.to_rss(rss, current) - end - - def taxo_topic - @taxo_topics[0] and @taxo_topics[0].value - end - - def taxo_topic=(new_value) - @taxo_topic[0] = self.class::TaxonomyTopic.new(self) - @taxo_topic[0].value = new_value - end -EOC + class_name = "TaxonomyTopics" + klass.def_classed_elements("#{TAXO_PREFIX}_topic", "value", class_name) end - + def self.install_taxo_topic(klass) - klass.module_eval(<<-EOC, *Utils.get_file_and_line_from_caller(1)) + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) class TaxonomyTopics < TaxonomyTopicsBase class TaxonomyTopic < TaxonomyTopicBase DublinCoreModel.install_dublin_core(self) TaxonomyTopicsModel.install_taxo_topics(self) - def to_rss(rss, current) + def to_feed(feed, current) if current.respond_to?(:taxo_topics) topic = current.class::TaxonomyTopic.new(value) topic.taxo_link = value - taxo_topics.to_rss(rss, topic) if taxo_topics + taxo_topics.to_feed(feed, topic) if taxo_topics current.taxo_topics << topic - setup_other_elements(rss) + setup_other_elements(feed, topic) end end - - def current_element(rss) - super.taxo_topics.last - end end end EOC end - class TaxonomyTopicsBase - include Base - - def_array_element("taxo_topics") - - def new_taxo_topic - taxo_topic = self.class::TaxonomyTopic.new(self) - @taxo_topics << taxo_topic - if block_given? - yield taxo_topic - else - taxo_topic - end - end + class TaxonomyTopicsBase < Base + def_array_element("topic", nil, "TaxonomyTopic") + alias_method(:new_taxo_topic, :new_topic) # For backward compatibility - def to_rss(rss, current) - @taxo_topics.each do |taxo_topic| - taxo_topic.to_rss(rss, current) - end - end - - class TaxonomyTopicBase - include Base + class TaxonomyTopicBase < Base include DublinCoreModel include TaxonomyTopicsModel - + attr_accessor :value add_need_initialize_variable("value") alias_method(:taxo_link, :value) alias_method(:taxo_link=, :value=) - + def have_required_values? @value end @@ -140,43 +89,31 @@ EOC class RSSBase include TaxonomyTopicModel end - + class ChannelBase include TaxonomyTopicsModel end - + class ItemsBase class ItemBase include TaxonomyTopicsModel end end - class RSS10 - TaxonomyTopicModel.install_taxo_topic(self) - - class Channel - TaxonomyTopicsModel.install_taxo_topics(self) - end + makers.each do |maker| + maker.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + TaxonomyTopicModel.install_taxo_topic(self) - class Items - class Item + class Channel TaxonomyTopicsModel.install_taxo_topics(self) end - end - end - - class RSS09 - TaxonomyTopicModel.install_taxo_topic(self) - - class Channel - TaxonomyTopicsModel.install_taxo_topics(self) - end - class Items - class Item - TaxonomyTopicsModel.install_taxo_topics(self) + class Items + class Item + TaxonomyTopicsModel.install_taxo_topics(self) + end end - end + EOC end end end diff --git a/lib/rss/maker/trackback.rb b/lib/rss/maker/trackback.rb index 4ae6164f68..f97691c608 100644 --- a/lib/rss/maker/trackback.rb +++ b/lib/rss/maker/trackback.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require 'rss/trackback' require 'rss/maker/1.0' require 'rss/maker/2.0' @@ -8,123 +9,54 @@ module RSS def self.append_features(klass) super - name = "#{RSS::TRACKBACK_PREFIX}_ping" - klass.add_need_initialize_variable(name) - klass.add_other_element(name) - klass.module_eval(<<-EOC, __FILE__, __LINE__+1) - attr_accessor :#{name} - def setup_#{name}(rss, current) - if #{name} and current.respond_to?(:#{name}=) - current.#{name} = #{name} - end - end - EOC - - name = "#{RSS::TRACKBACK_PREFIX}_abouts" - klass.add_need_initialize_variable(name, "make_#{name}") - klass.add_other_element(name) - klass.module_eval(<<-EOC, __FILE__, __LINE__+1) - attr_accessor :#{name} - def make_#{name} - self.class::TrackBackAbouts.new(self) - end - - def setup_#{name}(rss, current) - @#{name}.to_rss(rss, current) - end - EOC + klass.def_other_element("#{RSS::TRACKBACK_PREFIX}_ping") + klass.def_classed_elements("#{RSS::TRACKBACK_PREFIX}_about", "value", + "TrackBackAbouts") end - class TrackBackAboutsBase - include Base - - def_array_element("abouts") - - def new_about - about = self.class::TrackBackAbout.new(@maker) - @abouts << about - if block_given? - yield about - else - about - end - end - - def to_rss(rss, current) - @abouts.each do |about| - about.to_rss(rss, current) - end - end - - class TrackBackAboutBase - include Base + class TrackBackAboutsBase < Base + def_array_element("about", nil, "TrackBackAbout") + class TrackBackAboutBase < Base attr_accessor :value add_need_initialize_variable("value") - + alias_method(:resource, :value) alias_method(:resource=, :value=) alias_method(:content, :value) alias_method(:content=, :value=) - + def have_required_values? @value end - - end - end - end - - class ItemsBase - class ItemBase; include TrackBackModel; end - end - class RSS10 - class Items - class Item - class TrackBackAbouts < TrackBackAboutsBase - class TrackBackAbout < TrackBackAboutBase - def to_rss(rss, current) - if resource - about = ::RSS::TrackBackModel10::TrackBackAbout.new(resource) - current.trackback_abouts << about - end - end + def to_feed(feed, current) + if current.respond_to?(:trackback_abouts) and have_required_values? + about = current.class::TrackBackAbout.new + setup_values(about) + setup_other_elements(about) + current.trackback_abouts << about end end end end end - class RSS09 - class Items - class Item - class TrackBackAbouts < TrackBackAboutsBase - def to_rss(*args) - end - class TrackBackAbout < TrackBackAboutBase - end - end - end - end + class ItemsBase + class ItemBase; include TrackBackModel; end end - - class RSS20 - class Items - class Item - class TrackBackAbouts < TrackBackAboutsBase - class TrackBackAbout < TrackBackAboutBase - def to_rss(rss, current) - if content - about = ::RSS::TrackBackModel20::TrackBackAbout.new(content) - current.trackback_abouts << about - end + + makers.each do |maker| + maker.module_eval(<<-EOC, __FILE__, __LINE__ + 1) + class Items + class Item + class TrackBackAbouts < TrackBackAboutsBase + class TrackBackAbout < TrackBackAboutBase end end end end - end + EOC end - end end diff --git a/lib/rss/parser.rb b/lib/rss/parser.rb index 033bc123aa..a9842e6d40 100644 --- a/lib/rss/parser.rb +++ b/lib/rss/parser.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: false require "forwardable" require "open-uri" require "rss/rss" +require "rss/xml" module RSS @@ -33,8 +35,8 @@ module RSS class NotValidXMLParser < Error def initialize(parser) super("#{parser} is not an available XML parser. " << - "Available XML parser"<< - (AVAILABLE_PARSERS.size > 1 ? "s are ": " is ") << + "Available XML parser" << + (AVAILABLE_PARSERS.size > 1 ? "s are " : " is ") << "#{AVAILABLE_PARSERS.inspect}.") end end @@ -97,7 +99,7 @@ module RSS return rss if maybe_xml?(rss) uri = to_uri(rss) - + if uri.respond_to?(:read) uri.read elsif !rss.tainted? and File.readable?(rss) @@ -112,13 +114,13 @@ module RSS source.is_a?(String) and /</ =~ source end - # Attempt to convert rss to a URI, but just return it if + # Attempt to convert rss to a URI, but just return it if # there's a ::URI::Error def to_uri(rss) return rss if rss.is_a?(::URI::Generic) begin - URI(rss) + ::URI.parse(rss) rescue ::URI::Error rss end @@ -132,7 +134,7 @@ module RSS listener.raise_for_undefined_entity? end end - + def initialize(rss) @listener = self.class.listener.new @rss = rss @@ -173,35 +175,35 @@ module RSS class << self - @@setters = {} + @@accessor_bases = {} @@registered_uris = {} @@class_names = {} # return the setter for the uri, tag_name pair, or nil. def setter(uri, tag_name) - begin - @@setters[uri][tag_name] - rescue NameError + _getter = getter(uri, tag_name) + if _getter + "#{_getter}=" + else nil end end + def getter(uri, tag_name) + (@@accessor_bases[uri] || {})[tag_name] + end # return the tag_names for setters associated with uri def available_tags(uri) - begin - @@setters[uri].keys - rescue NameError - [] - end + (@@accessor_bases[uri] || {}).keys end - + # register uri against this name. def register_uri(uri, name) @@registered_uris[name] ||= {} @@registered_uris[name][uri] = nil end - + # test if this uri is registered against this name def uri_registered?(uri, name) @@registered_uris[name].has_key?(uri) @@ -216,52 +218,48 @@ module RSS # retrieve class_name for the supplied uri and tag_name # If it doesn't exist, capitalize the tag_name def class_name(uri, tag_name) - begin - @@class_names[uri][tag_name] - rescue NameError - tag_name[0,1].upcase + tag_name[1..-1] - end + name = (@@class_names[uri] || {})[tag_name] + return name if name + + tag_name = tag_name.gsub(/[_\-]([a-z]?)/) {$1.upcase} + tag_name[0, 1].upcase + tag_name[1..-1] end - def install_get_text_element(uri, name, setter) - install_setter(uri, name, setter) + def install_get_text_element(uri, name, accessor_base) + install_accessor_base(uri, name, accessor_base) def_get_text_element(uri, name, *get_file_and_line_from_caller(1)) end - + def raise_for_undefined_entity? true end - + private - # set the setter for the uri, tag_name pair - def install_setter(uri, tag_name, setter) - @@setters[uri] ||= {} - @@setters[uri][tag_name] = setter + # set the accessor for the uri, tag_name pair + def install_accessor_base(uri, tag_name, accessor_base) + @@accessor_bases[uri] ||= {} + @@accessor_bases[uri][tag_name] = accessor_base.chomp("=") end - def def_get_text_element(uri, name, file, line) - register_uri(uri, name) - unless private_instance_methods(false).include?("start_#{name}") - module_eval(<<-EOT, file, line) - def start_#{name}(name, prefix, attrs, ns) + def def_get_text_element(uri, element_name, file, line) + register_uri(uri, element_name) + method_name = "start_#{element_name}" + unless private_method_defined?(method_name) + define_method(method_name) do |name, prefix, attrs, ns| uri = _ns(ns, prefix) - if self.class.uri_registered?(uri, #{name.inspect}) + if self.class.uri_registered?(uri, element_name) start_get_text_element(name, prefix, ns, uri) else start_else_element(name, prefix, attrs, ns) end end - EOT - __send__("private", "start_#{name}") + private(method_name) end end - end - end module ListenerMixin - attr_reader :rss attr_accessor :ignore_unknown_element @@ -271,15 +269,18 @@ module RSS @rss = nil @ignore_unknown_element = true @do_validate = true - @ns_stack = [{}] + @ns_stack = [{"xml" => :xml}] @tag_stack = [[]] @text_stack = [''] @proc_stack = [] @last_element = nil @version = @encoding = @standalone = nil @xml_stylesheets = [] + @xml_child_mode = false + @xml_element = nil + @last_xml_element = nil end - + # set instance vars for version, encoding, standalone def xmldecl(version, encoding, standalone) @version, @encoding, @standalone = version, encoding, standalone @@ -289,7 +290,7 @@ module RSS if name == "xml-stylesheet" params = parse_pi_content(content) if params.has_key?("href") - @xml_stylesheets << XMLStyleSheet.new(*params) + @xml_stylesheets << XMLStyleSheet.new(params) end end end @@ -311,10 +312,39 @@ module RSS prefix, local = split_name(name) @tag_stack.last.push([_ns(ns, prefix), local]) @tag_stack.push([]) - if respond_to?("start_#{local}", true) - __send__("start_#{local}", local, prefix, attrs, ns.dup) + if @xml_child_mode + previous = @last_xml_element + element_attrs = attributes.dup + unless previous + ns.each do |ns_prefix, value| + next if ns_prefix == "xml" + key = ns_prefix.empty? ? "xmlns" : "xmlns:#{ns_prefix}" + element_attrs[key] ||= value + end + end + next_element = XML::Element.new(local, + prefix.empty? ? nil : prefix, + _ns(ns, prefix), + element_attrs) + previous << next_element if previous + @last_xml_element = next_element + pr = Proc.new do |text, tags| + if previous + @last_xml_element = previous + else + @xml_element = @last_xml_element + @last_xml_element = nil + end + end + @proc_stack.push(pr) else - start_else_element(local, prefix, attrs, ns.dup) + if @rss.nil? and respond_to?("initial_start_#{local}", true) + __send__("initial_start_#{local}", local, prefix, attrs, ns.dup) + elsif respond_to?("start_#{local}", true) + __send__("start_#{local}", local, prefix, attrs, ns.dup) + else + start_else_element(local, prefix, attrs, ns.dup) + end end end @@ -331,7 +361,11 @@ module RSS end def text(data) - @text_stack.last << data + if @xml_child_mode + @last_xml_element << data if @last_xml_element + else + @text_stack.last << data + end end private @@ -354,12 +388,12 @@ module RSS def start_else_element(local, prefix, attrs, ns) class_name = self.class.class_name(_ns(ns, prefix), local) current_class = @last_element.class - if current_class.constants.include?(class_name) + if known_class?(current_class, class_name) next_class = current_class.const_get(class_name) start_have_something_element(local, prefix, attrs, ns, next_class) else if !@do_validate or @ignore_unknown_element - @proc_stack.push(nil) + @proc_stack.push(setup_next_element_in_unknown_element) else parent = "ROOT ELEMENT???" if current_class.tag_name @@ -370,26 +404,56 @@ module RSS end end - NAMESPLIT = /^(?:([\w:][-\w\d.]*):)?([\w:][-\w\d.]*)/ + if Module.method(:const_defined?).arity == -1 + def known_class?(target_class, class_name) + class_name and + (target_class.const_defined?(class_name, false) or + target_class.constants.include?(class_name.to_sym)) + end + else + def known_class?(target_class, class_name) + class_name and + (target_class.const_defined?(class_name) or + target_class.constants.include?(class_name)) + end + end + + NAMESPLIT = /^(?:([\w:][-\w.]*):)?([\w:][-\w.]*)/ def split_name(name) name =~ NAMESPLIT [$1 || '', $2] end - def check_ns(tag_name, prefix, ns, require_uri) - if @do_validate - if _ns(ns, prefix) == require_uri - #ns.delete(prefix) - else + def check_ns(tag_name, prefix, ns, require_uri, ignore_unknown_element=nil) + if _ns(ns, prefix) == require_uri + true + else + if ignore_unknown_element.nil? + ignore_unknown_element = @ignore_unknown_element + end + + if ignore_unknown_element + false + elsif @do_validate raise NSError.new(tag_name, prefix, require_uri) + else + # Force bind required URI with prefix + @ns_stack.last[prefix] = require_uri + true end end end def start_get_text_element(tag_name, prefix, ns, required_uri) - @proc_stack.push Proc.new {|text, tags| + pr = Proc.new do |text, tags| setter = self.class.setter(required_uri, tag_name) - if @last_element.respond_to?(setter) + if setter and @last_element.respond_to?(setter) + if @do_validate + getter = self.class.getter(required_uri, tag_name) + if @last_element.__send__(getter) + raise TooMuchTagError.new(tag_name, @last_element.tag_name) + end + end @last_element.__send__(setter, text.to_s) else if @do_validate and !@ignore_unknown_element @@ -397,16 +461,22 @@ module RSS @last_element.tag_name) end end - } + end + @proc_stack.push(pr) end def start_have_something_element(tag_name, prefix, attrs, ns, klass) + if check_ns(tag_name, prefix, ns, klass.required_uri) + attributes = collect_attributes(tag_name, prefix, attrs, ns, klass) + @proc_stack.push(setup_next_element(tag_name, klass, attributes)) + else + @proc_stack.push(setup_next_element_in_unknown_element) + end + end - check_ns(tag_name, prefix, ns, klass.required_uri) - + def collect_attributes(tag_name, prefix, attrs, ns, klass) attributes = {} klass.get_attributes.each do |a_name, a_uri, required, element_name| - if a_uri.is_a?(String) or !a_uri.respond_to?(:include?) a_uri = [a_uri] end @@ -435,24 +505,48 @@ module RSS attributes[a_name] = val end + attributes + end + def setup_next_element(tag_name, klass, attributes) previous = @last_element next_element = klass.new(@do_validate, attributes) - previous.instance_eval {set_next_element(tag_name, next_element)} + previous.set_next_element(tag_name, next_element) @last_element = next_element - @proc_stack.push Proc.new { |text, tags| + @last_element.parent = previous if klass.need_parent? + @xml_child_mode = @last_element.have_xml_content? + + Proc.new do |text, tags| p(@last_element.class) if DEBUG - @last_element.content = text if klass.have_content? + if @xml_child_mode + @last_element.content = @xml_element.to_s + xml_setter = @last_element.class.xml_setter + @last_element.__send__(xml_setter, @xml_element) + @xml_element = nil + @xml_child_mode = false + else + if klass.have_content? + if @last_element.need_base64_encode? + text = text.lstrip.unpack("m").first + end + @last_element.content = text + end + end if @do_validate @last_element.validate_for_stream(tags, @ignore_unknown_element) end @last_element = previous - } + end end + def setup_next_element_in_unknown_element + current_element, @last_element = @last_element, nil + Proc.new {@last_element = current_element} + end end unless const_defined? :AVAILABLE_PARSER_LIBRARIES + # The list of all available libraries for parsing. AVAILABLE_PARSER_LIBRARIES = [ ["rss/xmlparser", :XMLParserParser], ["rss/xmlscanner", :XMLScanParser], @@ -460,6 +554,7 @@ module RSS ] end + # The list of all available parsers, in constant form. AVAILABLE_PARSERS = [] AVAILABLE_PARSER_LIBRARIES.each do |lib, parser| diff --git a/lib/rss/rexmlparser.rb b/lib/rss/rexmlparser.rb index 4dabf59199..ef0595e447 100644 --- a/lib/rss/rexmlparser.rb +++ b/lib/rss/rexmlparser.rb @@ -1,13 +1,9 @@ +# frozen_string_literal: false require "rexml/document" require "rexml/streamlistener" -/\A(\d+)\.(\d+)(?:\.\d+)+\z/ =~ REXML::Version -if ([$1.to_i, $2.to_i] <=> [2, 5]) < 0 - raise LoadError, "needs REXML 2.5 or later (#{REXML::Version})" -end - module RSS - + class REXMLParser < BaseParser class << self @@ -15,7 +11,7 @@ module RSS REXMLListener end end - + private def _parse begin @@ -28,9 +24,9 @@ module RSS raise NotWellFormedError.new(line){e.message} end end - + end - + class REXMLListener < BaseListener include REXML::StreamListener @@ -41,7 +37,7 @@ module RSS false end end - + def xmldecl(version, encoding, standalone) super(version, encoding, standalone == "yes") # Encoding is converted to UTF-8 when REXML parse XML. diff --git a/lib/rss/rss.rb b/lib/rss/rss.rb index a06985af94..db87e11ad5 100644 --- a/lib/rss/rss.rb +++ b/lib/rss/rss.rb @@ -1,8 +1,17 @@ +# frozen_string_literal: false require "time" class Time class << self unless respond_to?(:w3cdtf) + # This method converts a W3CDTF string date/time format to Time object. + # + # The W3CDTF format is defined here: http://www.w3.org/TR/NOTE-datetime + # + # Time.w3cdtf('2003-02-15T13:50:05-05:00') + # # => 2003-02-15 10:50:05 -0800 + # Time.w3cdtf('2003-02-15T13:50:05-05:00').class + # # => Time def w3cdtf(date) if /\A\s* (-?\d+)-(\d\d)-(\d\d) @@ -11,11 +20,19 @@ class Time (\.\d+)? (Z|[+-]\d\d:\d\d)?)? \s*\z/ix =~ date and (($5 and $8) or (!$5 and !$8)) - datetime = [$1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i] - datetime << $7.to_f * 1000000 if $7 - if $8 - Time.utc(*datetime) - zone_offset($8) + datetime = [$1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i] + usec = 0 + usec = $7.to_f * 1000000 if $7 + zone = $8 + if zone + off = zone_offset(zone, datetime[0]) + datetime = apply_offset(*(datetime + [off])) + datetime << usec + time = Time.utc(*datetime) + force_zone!(time, zone, off) + time else + datetime << usec Time.local(*datetime) end else @@ -25,11 +42,26 @@ class Time end end - unless instance_methods.include?("w3cdtf") - alias w3cdtf iso8601 + unless method_defined?(:w3cdtf) + # This method converts a Time object to a String. The String contains the + # time in W3CDTF date/time format. + # + # The W3CDTF format is defined here: http://www.w3.org/TR/NOTE-datetime + # + # Time.now.w3cdtf + # # => "2013-08-26T14:12:10.817124-07:00" + def w3cdtf + if usec.zero? + fraction_digits = 0 + else + fraction_digits = strftime('%6N').index(/0*\z/) + end + xmlschema(fraction_digits) + end end end + require "English" require "rss/utils" require "rss/converter" @@ -37,14 +69,20 @@ require "rss/xml-stylesheet" module RSS - VERSION = "0.1.6" + # The current version of RSS + VERSION = "0.2.7" + # The URI of the RSS 1.0 specification URI = "http://purl.org/rss/1.0/" - DEBUG = false + DEBUG = false # :nodoc: + # The basic error all other RSS errors stem from. Rescue this error if you + # want to handle any given RSS error and you don't care about the details. class Error < StandardError; end + # RSS, being an XML-based format, has namespace support. If two namespaces are + # declared with the same name, an OverlappedPrefixError will be raised. class OverlappedPrefixError < Error attr_reader :prefix def initialize(prefix) @@ -52,8 +90,13 @@ module RSS end end + # The InvalidRSSError error is the base class for a variety of errors + # related to a poorly-formed RSS feed. Rescue this error if you only + # care that a file could be invalid, but don't care how it is invalid. class InvalidRSSError < Error; end + # Since RSS is based on XML, it must have opening and closing tags that + # match. If they don't, a MissingTagError will be raised. class MissingTagError < InvalidRSSError attr_reader :tag, :parent def initialize(tag, parent) @@ -62,6 +105,9 @@ module RSS end end + # Some tags must only exist a specific number of times in a given RSS feed. + # If a feed has too many occurrences of one of these tags, a TooMuchTagError + # will be raised. class TooMuchTagError < InvalidRSSError attr_reader :tag, :parent def initialize(tag, parent) @@ -70,6 +116,8 @@ module RSS end end + # Certain attributes are required on specific tags in an RSS feed. If a feed + # is missing one of these attributes, a MissingAttributeError is raised. class MissingAttributeError < InvalidRSSError attr_reader :tag, :attribute def initialize(tag, attribute) @@ -78,6 +126,8 @@ module RSS end end + # RSS does not allow for free-form tag names, so if an RSS feed contains a + # tag that we don't know about, an UnknownTagError is raised. class UnknownTagError < InvalidRSSError attr_reader :tag, :uri def initialize(tag, uri) @@ -86,6 +136,7 @@ module RSS end end + # Raised when an unexpected tag is encountered. class NotExpectedTagError < InvalidRSSError attr_reader :tag, :uri, :parent def initialize(tag, uri, parent) @@ -94,8 +145,10 @@ module RSS end end # For backward compatibility :X - NotExceptedTagError = NotExpectedTagError + NotExceptedTagError = NotExpectedTagError # :nodoc: + # Attributes are in key-value form, and if there's no value provided for an + # attribute, a NotAvailableValueError will be raised. class NotAvailableValueError < InvalidRSSError attr_reader :tag, :value, :attribute def initialize(tag, value, attribute=nil) @@ -107,6 +160,7 @@ module RSS end end + # Raised when an unknown conversion error occurs. class UnknownConversionMethodError < Error attr_reader :to, :from def initialize(to, from) @@ -116,8 +170,9 @@ module RSS end end # for backward compatibility - UnknownConvertMethod = UnknownConversionMethodError + UnknownConvertMethod = UnknownConversionMethodError # :nodoc: + # Raised when a conversion failure occurs. class ConversionError < Error attr_reader :string, :to, :from def initialize(string, to, from) @@ -128,6 +183,7 @@ module RSS end end + # Raised when a required variable is not set. class NotSetError < Error attr_reader :name, :variables def initialize(name, variables) @@ -136,17 +192,27 @@ module RSS super("required variables of #{@name} are not set: #{@variables.join(', ')}") end end - - module BaseModel + # Raised when a RSS::Maker attempts to use an unknown maker. + class UnsupportedMakerVersionError < Error + attr_reader :version + def initialize(version) + @version = version + super("Maker doesn't support version: #{@version}") + end + end + + module BaseModel include Utils - def install_have_child_element(tag_name, uri, occurs, name=nil) + def install_have_child_element(tag_name, uri, occurs, name=nil, type=nil) name ||= tag_name add_need_initialize_variable(name) install_model(tag_name, uri, occurs, name) - attr_accessor name + writer_type, reader_type = type + def_corresponded_attr_writer name, writer_type + def_corresponded_attr_reader name, reader_type install_element(name) do |n, elem_name| <<-EOC if @#{n} @@ -164,7 +230,7 @@ EOC plural_name ||= "#{name}s" add_have_children_element(name, plural_name) add_plural_form(name, plural_name) - install_model(tag_name, uri, occurs, plural_name) + install_model(tag_name, uri, occurs, plural_name, true) def_children_accessor(name, plural_name) install_element(name, "s") do |n, elem_name| @@ -179,26 +245,32 @@ EOC end end - def install_text_element(tag_name, uri, occurs, name=nil, type=nil, disp_name=nil) + def install_text_element(tag_name, uri, occurs, name=nil, type=nil, + disp_name=nil) name ||= tag_name disp_name ||= name - self::ELEMENTS << name + self::ELEMENTS << name unless self::ELEMENTS.include?(name) add_need_initialize_variable(name) install_model(tag_name, uri, occurs, name) - def_corresponded_attr_writer name, type, disp_name - convert_attr_reader name + def_corresponded_attr_writer(name, type, disp_name) + def_corresponded_attr_reader(name, type || :convert) install_element(name) do |n, elem_name| <<-EOC - if @#{n} + if respond_to?(:#{n}_content) + content = #{n}_content + else + content = @#{n} + end + if content rv = "\#{indent}<#{elem_name}>" - value = html_escape(@#{n}) + value = html_escape(content) if need_convert rv << convert(value) else rv << value end - rv << "</#{elem_name}>" + rv << "</#{elem_name}>" rv else '' @@ -218,7 +290,7 @@ EOC # accessor convert_attr_reader name date_writer(name, type, disp_name) - + install_element(name) do |n, elem_name| <<-EOC if @#{n} @@ -229,7 +301,7 @@ EOC else rv << value end - rv << "</#{elem_name}>" + rv << "</#{elem_name}>" rv else '' @@ -252,26 +324,106 @@ EOC EOC end - def convert_attr_reader(*attrs) + def inherit_convert_attr_reader(*attrs) + attrs.each do |attr| + module_eval(<<-EOC, *get_file_and_line_from_caller(2)) + def #{attr}_without_inherit + convert(@#{attr}) + end + + def #{attr} + if @#{attr} + #{attr}_without_inherit + elsif @parent + @parent.#{attr} + else + nil + end + end +EOC + end + end + + def uri_convert_attr_reader(*attrs) attrs.each do |attr| - attr = attr.id2name if attr.kind_of?(Integer) module_eval(<<-EOC, *get_file_and_line_from_caller(2)) + def #{attr}_without_base + convert(@#{attr}) + end + def #{attr} - if @converter - @converter.convert(@#{attr}) + value = #{attr}_without_base + return nil if value.nil? + if /\\A[a-z][a-z0-9+.\\-]*:/i =~ value + value else - @#{attr} + "\#{base}\#{value}" end end EOC end end + def convert_attr_reader(*attrs) + attrs.each do |attr| + module_eval(<<-EOC, *get_file_and_line_from_caller(2)) + def #{attr} + convert(@#{attr}) + end +EOC + end + end + + def explicit_clean_other_attr_reader(*attrs) + attrs.each do |attr| + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + attr_reader(:#{attr}) + def #{attr}? + ExplicitCleanOther.parse(@#{attr}) + end + EOC + end + end + + def yes_other_attr_reader(*attrs) + attrs.each do |attr| + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + attr_reader(:#{attr}) + def #{attr}? + Utils::YesOther.parse(@#{attr}) + end + EOC + end + end + + def csv_attr_reader(*attrs) + separator = nil + if attrs.last.is_a?(Hash) + options = attrs.pop + separator = options[:separator] + end + separator ||= ", " + attrs.each do |attr| + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + attr_reader(:#{attr}) + def #{attr}_content + if @#{attr}.nil? + @#{attr} + else + @#{attr}.join(#{separator.dump}) + end + end + EOC + end + end + def date_writer(name, type, disp_name=name) module_eval(<<-EOC, *get_file_and_line_from_caller(2)) def #{name}=(new_value) - if new_value.nil? or new_value.kind_of?(Time) + if new_value.nil? @#{name} = new_value + elsif new_value.kind_of?(Time) + @#{name} = new_value.dup else if @do_validate begin @@ -283,7 +435,9 @@ EOC @#{name} = nil if /\\A\\s*\\z/ !~ new_value.to_s begin - @#{name} = Time.parse(new_value) + unless Date._parse(new_value, false).empty? + @#{name} = Time.parse(new_value) + end rescue ArgumentError end end @@ -364,6 +518,68 @@ EOC EOC end + def text_type_writer(name, disp_name=name) + module_eval(<<-EOC, *get_file_and_line_from_caller(2)) + def #{name}=(new_value) + if @do_validate and + !["text", "html", "xhtml", nil].include?(new_value) + raise NotAvailableValueError.new('#{disp_name}', new_value) + end + @#{name} = new_value + end +EOC + end + + def content_writer(name, disp_name=name) + klass_name = "self.class::#{Utils.to_class_name(name)}" + module_eval(<<-EOC, *get_file_and_line_from_caller(2)) + def #{name}=(new_value) + if new_value.is_a?(#{klass_name}) + @#{name} = new_value + else + @#{name} = #{klass_name}.new + @#{name}.content = new_value + end + end +EOC + end + + def explicit_clean_other_writer(name, disp_name=name) + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{name}=(value) + value = (value ? "yes" : "no") if [true, false].include?(value) + @#{name} = value + end + EOC + end + + def yes_other_writer(name, disp_name=name) + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{name}=(new_value) + if [true, false].include?(new_value) + new_value = new_value ? "yes" : "no" + end + @#{name} = new_value + end + EOC + end + + def csv_writer(name, disp_name=name) + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{name}=(new_value) + @#{name} = Utils::CSV.parse(new_value) + end + EOC + end + + def csv_integer_writer(name, disp_name=name) + module_eval(<<-EOC, __FILE__, __LINE__ + 1) + def #{name}=(new_value) + @#{name} = Utils::CSV.parse(new_value) {|v| Integer(v)} + end + EOC + end + def def_children_accessor(accessor_name, plural_name) module_eval(<<-EOC, *get_file_and_line_from_caller(2)) def #{plural_name} @@ -379,10 +595,11 @@ EOC end def #{accessor_name}=(*args) - warn("Warning:\#{caller.first.sub(/:in `.*'\z/, '')}: " \ - "Don't use `#{accessor_name} = XXX'/`set_#{accessor_name}(XXX)'. " \ + receiver = self.class.name + warn("Don't use `\#{receiver}\##{accessor_name} = XXX'/" \ + "`\#{receiver}\#set_#{accessor_name}(XXX)'. " \ "Those APIs are not sense of Ruby. " \ - "Use `#{plural_name} << XXX' instead of them.") + "Use `\#{receiver}\##{plural_name} << XXX' instead of them.", uplevel: 1) if args.size == 1 @#{accessor_name}.push(args[0]) else @@ -394,13 +611,64 @@ EOC end end - class Element + module SetupMaker + def setup_maker(maker) + target = maker_target(maker) + unless target.nil? + setup_maker_attributes(target) + setup_maker_element(target) + setup_maker_elements(target) + end + end + private + def maker_target(maker) + nil + end + + def setup_maker_attributes(target) + end + + def setup_maker_element(target) + self.class.need_initialize_variables.each do |var| + value = __send__(var) + next if value.nil? + if value.respond_to?("setup_maker") and + !not_need_to_call_setup_maker_variables.include?(var) + value.setup_maker(target) + else + setter = "#{var}=" + if target.respond_to?(setter) + target.__send__(setter, value) + end + end + end + end + + def not_need_to_call_setup_maker_variables + [] + end + + def setup_maker_elements(parent) + self.class.have_children_elements.each do |name, plural_name| + if parent.respond_to?(plural_name) + target = parent.__send__(plural_name) + __send__(plural_name).each do |elem| + elem.setup_maker(target) + end + end + end + end + end + + class Element extend BaseModel include Utils + extend Utils::InheritedReader + include SetupMaker INDENT = " " - + MUST_CALL_VALIDATORS = {} MODELS = [] GET_ATTRIBUTES = [] @@ -408,140 +676,149 @@ EOC TO_ELEMENT_METHODS = [] NEED_INITIALIZE_VARIABLES = [] PLURAL_FORMS = {} - - class << self + class << self def must_call_validators - MUST_CALL_VALIDATORS + inherited_hash_reader("MUST_CALL_VALIDATORS") end def models - MODELS + inherited_array_reader("MODELS") end def get_attributes - GET_ATTRIBUTES + inherited_array_reader("GET_ATTRIBUTES") end def have_children_elements - HAVE_CHILDREN_ELEMENTS + inherited_array_reader("HAVE_CHILDREN_ELEMENTS") end def to_element_methods - TO_ELEMENT_METHODS + inherited_array_reader("TO_ELEMENT_METHODS") end def need_initialize_variables - NEED_INITIALIZE_VARIABLES + inherited_array_reader("NEED_INITIALIZE_VARIABLES") end def plural_forms - PLURAL_FORMS + inherited_hash_reader("PLURAL_FORMS") + end + + def inherited_base + ::RSS::Element end - def inherited(klass) - klass.const_set("MUST_CALL_VALIDATORS", {}) - klass.const_set("MODELS", []) - klass.const_set("GET_ATTRIBUTES", []) - klass.const_set("HAVE_CHILDREN_ELEMENTS", []) - klass.const_set("TO_ELEMENT_METHODS", []) - klass.const_set("NEED_INITIALIZE_VARIABLES", []) - klass.const_set("PLURAL_FORMS", {}) + klass.const_set(:MUST_CALL_VALIDATORS, {}) + klass.const_set(:MODELS, []) + klass.const_set(:GET_ATTRIBUTES, []) + klass.const_set(:HAVE_CHILDREN_ELEMENTS, []) + klass.const_set(:TO_ELEMENT_METHODS, []) + klass.const_set(:NEED_INITIALIZE_VARIABLES, []) + klass.const_set(:PLURAL_FORMS, {}) + + tag_name = klass.name.split(/::/).last + tag_name[0, 1] = tag_name[0, 1].downcase + klass.instance_variable_set(:@tag_name, tag_name) + klass.instance_variable_set(:@have_content, false) + end - klass.module_eval(<<-EOC) - public - - @tag_name = name.split(/::/).last - @tag_name[0,1] = @tag_name[0,1].downcase - @have_content = false + def install_must_call_validator(prefix, uri) + self::MUST_CALL_VALIDATORS[uri] = prefix + end - def self.must_call_validators - super.merge(MUST_CALL_VALIDATORS) - end - def self.models - MODELS + super - end - def self.get_attributes - GET_ATTRIBUTES + super - end - def self.have_children_elements - HAVE_CHILDREN_ELEMENTS + super - end - def self.to_element_methods - TO_ELEMENT_METHODS + super - end - def self.need_initialize_variables - NEED_INITIALIZE_VARIABLES + super - end - def self.plural_forms - super.merge(PLURAL_FORMS) + def install_model(tag, uri, occurs=nil, getter=nil, plural=false) + getter ||= tag + if m = self::MODELS.find {|t, u, o, g, p| t == tag and u == uri} + m[2] = occurs + else + self::MODELS << [tag, uri, occurs, getter, plural] end + end - - def self.install_must_call_validator(prefix, uri) - MUST_CALL_VALIDATORS[uri] = prefix + def install_get_attribute(name, uri, required=true, + type=nil, disp_name=nil, + element_name=nil) + disp_name ||= name + element_name ||= name + writer_type, reader_type = type + def_corresponded_attr_writer name, writer_type, disp_name + def_corresponded_attr_reader name, reader_type + if type == :boolean and /^is/ =~ name + alias_method "#{$POSTMATCH}?", name end - - def self.install_model(tag, uri, occurs=nil, getter=nil) - getter ||= tag - if m = MODELS.find {|t, u, o, g| t == tag and u == uri} - m[2] = occurs - else - MODELS << [tag, uri, occurs, getter] - end + self::GET_ATTRIBUTES << [name, uri, required, element_name] + add_need_initialize_variable(disp_name) + end + + def def_corresponded_attr_writer(name, type=nil, disp_name=nil) + disp_name ||= name + case type + when :integer + integer_writer name, disp_name + when :positive_integer + positive_integer_writer name, disp_name + when :boolean + boolean_writer name, disp_name + when :w3cdtf, :rfc822, :rfc2822 + date_writer name, type, disp_name + when :text_type + text_type_writer name, disp_name + when :content + content_writer name, disp_name + when :explicit_clean_other + explicit_clean_other_writer name, disp_name + when :yes_other + yes_other_writer name, disp_name + when :csv + csv_writer name + when :csv_integer + csv_integer_writer name + else + attr_writer name end + end - def self.install_get_attribute(name, uri, required=true, - type=nil, disp_name=nil, - element_name=nil) - disp_name ||= name - element_name ||= name - def_corresponded_attr_writer name, type, disp_name + def def_corresponded_attr_reader(name, type=nil) + case type + when :inherit + inherit_convert_attr_reader name + when :uri + uri_convert_attr_reader name + when :explicit_clean_other + explicit_clean_other_attr_reader name + when :yes_other + yes_other_attr_reader name + when :csv + csv_attr_reader name + when :csv_integer + csv_attr_reader name, :separator => "," + else convert_attr_reader name - if type == :boolean and /^is/ =~ name - alias_method "\#{$POSTMATCH}?", name - end - GET_ATTRIBUTES << [name, uri, required, element_name] - add_need_initialize_variable(disp_name) - end - - def self.def_corresponded_attr_writer(name, type=nil, disp_name=name) - case type - when :integer - integer_writer name, disp_name - when :positive_integer - positive_integer_writer name, disp_name - when :boolean - boolean_writer name, disp_name - when :w3cdtf, :rfc822, :rfc2822 - date_writer name, type, disp_name - else - attr_writer name - end end + end - def self.content_setup(type=nil) - def_corresponded_attr_writer "content", type - convert_attr_reader :content - @have_content = true - end + def content_setup(type=nil, disp_name=nil) + writer_type, reader_type = type + def_corresponded_attr_writer :content, writer_type, disp_name + def_corresponded_attr_reader :content, reader_type + @have_content = true + end - def self.have_content? - @have_content - end + def have_content? + @have_content + end - def self.add_have_children_element(variable_name, plural_name) - HAVE_CHILDREN_ELEMENTS << [variable_name, plural_name] - end - - def self.add_to_element_method(method_name) - TO_ELEMENT_METHODS << method_name - end + def add_have_children_element(variable_name, plural_name) + self::HAVE_CHILDREN_ELEMENTS << [variable_name, plural_name] + end - def self.add_need_initialize_variable(variable_name) - NEED_INITIALIZE_VARIABLES << variable_name - end - - def self.add_plural_form(singular, plural) - PLURAL_FORMS[singular] = plural - end - - EOC + def add_to_element_method(method_name) + self::TO_ELEMENT_METHODS << method_name + end + + def add_need_initialize_variable(variable_name) + self::NEED_INITIALIZE_VARIABLES << variable_name + end + + def add_plural_form(singular, plural) + self::PLURAL_FORMS[singular] = plural end def required_prefix @@ -551,7 +828,11 @@ EOC def required_uri "" end - + + def need_parent? + false + end + def install_ns(prefix, uri) if self::NSPOOL.has_key?(prefix) raise OverlappedPrefixError.new(prefix) @@ -564,12 +845,16 @@ EOC end end - attr_accessor :do_validate + attr_accessor :parent, :do_validate - def initialize(do_validate=true, attrs={}) + def initialize(do_validate=true, attrs=nil) + @parent = nil @converter = nil + if attrs.nil? and (do_validate.is_a?(Hash) or do_validate.is_a?(Array)) + do_validate, attrs = true, do_validate + end @do_validate = do_validate - initialize_variables(attrs) + initialize_variables(attrs || {}) end def tag_name @@ -579,7 +864,7 @@ EOC def full_name tag_name end - + def converter=(converter) @converter = converter targets = children.dup @@ -598,31 +883,37 @@ EOC value end end - + + def valid?(ignore_unknown_element=true) + validate(ignore_unknown_element) + true + rescue RSS::Error + false + end + def validate(ignore_unknown_element=true) + do_validate = @do_validate + @do_validate = true validate_attribute __validate(ignore_unknown_element) + ensure + @do_validate = do_validate end - + def validate_for_stream(tags, ignore_unknown_element=true) validate_attribute __validate(ignore_unknown_element, tags, false) end - def setup_maker(maker) - target = maker_target(maker) - unless target.nil? - setup_maker_attributes(target) - setup_maker_element(target) - setup_maker_elements(target) - end - end - def to_s(need_convert=true, indent='') if self.class.have_content? - return "" unless @content + return "" if !empty_content? and !content_is_set? rv = tag(indent) do |next_indent| - h(@content) + if empty_content? + "" + else + xmled_content + end end else rv = tag(indent) do |next_indent| @@ -635,6 +926,44 @@ EOC rv end + def have_xml_content? + false + end + + def need_base64_encode? + false + end + + def set_next_element(tag_name, next_element) + klass = next_element.class + prefix = "" + prefix << "#{klass.required_prefix}_" if klass.required_prefix + key = "#{prefix}#{tag_name.gsub(/-/, '_')}" + if self.class.plural_forms.has_key?(key) + ary = __send__("#{self.class.plural_forms[key]}") + ary << next_element + else + __send__("#{key}=", next_element) + end + end + + protected + def have_required_elements? + self.class::MODELS.all? do |tag, uri, occurs, getter| + if occurs.nil? or occurs == "+" + child = __send__(getter) + if child.is_a?(Array) + children = child + children.any? {|c| c.have_required_elements?} + else + not child.nil? + end + else + true + end + end + end + private def initialize_variables(attrs) normalized_attrs = {} @@ -646,16 +975,16 @@ EOC if value __send__("#{variable_name}=", value) else - instance_eval("@#{variable_name} = nil") + instance_variable_set("@#{variable_name}", nil) end end initialize_have_children_elements - @content = "" if self.class.have_content? + @content = normalized_attrs["content"] if self.class.have_content? end def initialize_have_children_elements self.class.have_children_elements.each do |variable_name, plural_name| - instance_eval("@#{variable_name} = []") + instance_variable_set("@#{variable_name}", []) end end @@ -665,8 +994,10 @@ EOC attrs = collect_attrs return "" if attrs.nil? + return "" unless have_required_elements? + attrs.update(additional_attrs) - start_tag = make_start_tag(indent, next_indent, attrs) + start_tag = make_start_tag(indent, next_indent, attrs.dup) if block content = block.call(next_indent) @@ -681,13 +1012,14 @@ EOC else content = content.reject{|x| x.empty?} if content.empty? + return "" if attrs.empty? end_tag = "/>" else start_tag << ">\n" end_tag = "\n#{indent}</#{full_name}>" end end - + start_tag + content.join("\n") + end_tag end @@ -712,7 +1044,7 @@ EOC end attrs end - + def tag_name_with_prefix(prefix) "#{prefix}:#{tag_name}" end @@ -722,56 +1054,6 @@ EOC '' end - def maker_target(maker) - nil - end - - def setup_maker_attributes(target) - end - - def setup_maker_element(target) - self.class.need_initialize_variables.each do |var| - value = __send__(var) - if value.respond_to?("setup_maker") and - !not_need_to_call_setup_maker_variables.include?(var) - value.setup_maker(target) - else - setter = "#{var}=" - if target.respond_to?(setter) - target.__send__(setter, value) - end - end - end - end - - def not_need_to_call_setup_maker_variables - [] - end - - def setup_maker_elements(parent) - self.class.have_children_elements.each do |name, plural_name| - if parent.respond_to?(plural_name) - target = parent.__send__(plural_name) - __send__(plural_name).each do |elem| - elem.setup_maker(target) - end - end - end - end - - def set_next_element(tag_name, next_element) - klass = next_element.class - prefix = "" - prefix << "#{klass.required_prefix}_" if klass.required_prefix - key = "#{prefix}#{tag_name}" - if self.class.plural_forms.has_key?(key) - ary = __send__("#{self.class.plural_forms[key]}") - ary << next_element - else - __send__("#{prefix}#{tag_name}=", next_element) - end - end - def children rv = [] self.class.models.each do |name, uri, occurs, getter| @@ -787,10 +1069,10 @@ EOC def _tags rv = [] - self.class.models.each do |name, uri, occurs, getter| + self.class.models.each do |name, uri, occurs, getter, plural| value = __send__(getter) next if value.nil? - if value.is_a?(Array) + if plural and value.is_a?(Array) rv.concat([[uri, name]] * value.size) else rv << [uri, name] @@ -817,7 +1099,7 @@ EOC must_call_validators.each do |uri, prefix| _validate(ignore_unknown_element, tags[uri], uri) meth = "#{prefix}_validate" - if respond_to?(meth, true) + if !prefix.empty? and respond_to?(meth, true) __send__(meth, ignore_unknown_element, tags[uri], uri) end end @@ -825,9 +1107,11 @@ EOC def validate_attribute _attrs.each do |a_name, required, alias_name| - if required and __send__(alias_name || a_name).nil? + value = instance_variable_get("@#{alias_name || a_name}") + if required and value.nil? raise MissingAttributeError.new(tag_name, a_name) end + __send__("#{alias_name || a_name}=", value) end end @@ -844,10 +1128,10 @@ EOC end models.each_with_index do |model, i| - name, model_uri, occurs, getter = model + name, _, occurs, = model if DEBUG - p "before" + p "before" p tags p model end @@ -933,23 +1217,51 @@ EOC rv end + def empty_content? + false + end + + def content_is_set? + if have_xml_content? + __send__(self.class.xml_getter) + else + content + end + end + + def xmled_content + if have_xml_content? + __send__(self.class.xml_getter).to_s + else + _content = content + _content = [_content].pack("m0") if need_base64_encode? + h(_content) + end + end end module RootElementMixin include XMLStyleSheetMixin - - attr_reader :output_encoding - def initialize(rss_version, version=nil, encoding=nil, standalone=nil) + attr_reader :output_encoding + attr_reader :feed_type, :feed_subtype, :feed_version + attr_accessor :version, :encoding, :standalone + def initialize(feed_version, version=nil, encoding=nil, standalone=nil) super() - @rss_version = rss_version + @feed_type = nil + @feed_subtype = nil + @feed_version = feed_version @version = version || '1.0' @encoding = encoding @standalone = standalone @output_encoding = nil end + def feed_info + [@feed_type, @feed_version, @feed_subtype] + end + def output_encoding=(enc) @output_encoding = enc self.converter = Converter.new(@output_encoding, @encoding) @@ -964,25 +1276,48 @@ EOC xss.setup_maker(maker) end - setup_maker_elements(maker) + super + end + + def to_feed(type, &block) + Maker.make(type) do |maker| + setup_maker(maker) + block.call(maker) if block + end + end + + def to_rss(type, &block) + to_feed("rss#{type}", &block) + end + + def to_atom(type, &block) + to_feed("atom:#{type}", &block) end - def to_xml(version=nil, &block) - if version.nil? or version == @rss_version + def to_xml(type=nil, &block) + if type.nil? or same_feed_type?(type) to_s else - RSS::Maker.make(version) do |maker| - setup_maker(maker) - block.call(maker) if block - end.to_s + to_feed(type, &block).to_s end end private + def same_feed_type?(type) + if /^(atom|rss)?(\d+\.\d+)?(?::(.+))?$/i =~ type + feed_type = ($1 || @feed_type).downcase + feed_version = $2 || @feed_version + feed_subtype = $3 || @feed_subtype + [feed_type, feed_version, feed_subtype] == feed_info + else + false + end + end + def tag(indent, attrs={}, &block) - rv = xmldecl + xml_stylesheet_pi - rv << super(indent, ns_declarations.merge(attrs), &block) - rv + rv = super(indent, ns_declarations.merge(attrs), &block) + return rv if rv.empty? + "#{xmldecl}#{xml_stylesheet_pi}#{rv}" end def xmldecl @@ -994,7 +1329,7 @@ EOC rv << "?>\n" rv end - + def ns_declarations decls = {} self.class::NSPOOL.collect do |prefix, uri| @@ -1003,13 +1338,9 @@ EOC end decls end - - def setup_maker_elements(maker) - channel.setup_maker(maker) if channel - image.setup_maker(maker) if image - textinput.setup_maker(maker) if textinput - super(maker) + + def maker_target(target) + target end end - end diff --git a/lib/rss/slash.rb b/lib/rss/slash.rb new file mode 100644 index 0000000000..0055fc9f88 --- /dev/null +++ b/lib/rss/slash.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: false +require 'rss/1.0' + +module RSS + # The prefix for the Slash XML namespace. + SLASH_PREFIX = 'slash' + # The URI of the Slash specification. + SLASH_URI = "http://purl.org/rss/1.0/modules/slash/" + + RDF.install_ns(SLASH_PREFIX, SLASH_URI) + + module SlashModel + extend BaseModel + + ELEMENT_INFOS = \ + [ + ["section"], + ["department"], + ["comments", :positive_integer], + ["hit_parade", :csv_integer], + ] + + class << self + def append_features(klass) + super + + return if klass.instance_of?(Module) + klass.install_must_call_validator(SLASH_PREFIX, SLASH_URI) + ELEMENT_INFOS.each do |name, type, *additional_infos| + full_name = "#{SLASH_PREFIX}_#{name}" + klass.install_text_element(full_name, SLASH_URI, "?", + full_name, type, name) + end + + klass.module_eval do + alias_method(:slash_hit_parades, :slash_hit_parade) + undef_method(:slash_hit_parade) + alias_method(:slash_hit_parade, :slash_hit_parade_content) + end + end + end + end + + class RDF + class Item; include SlashModel; end + end + + SlashModel::ELEMENT_INFOS.each do |name, type| + accessor_base = "#{SLASH_PREFIX}_#{name}" + BaseListener.install_get_text_element(SLASH_URI, name, accessor_base) + end +end diff --git a/lib/rss/syndication.rb b/lib/rss/syndication.rb index 93d35c89a7..8f9620f9f3 100644 --- a/lib/rss/syndication.rb +++ b/lib/rss/syndication.rb @@ -1,18 +1,20 @@ +# frozen_string_literal: false require "rss/1.0" module RSS - + # The prefix for the Syndication XML namespace. SY_PREFIX = 'sy' + # The URI of the Syndication specification. SY_URI = "http://purl.org/rss/1.0/modules/syndication/" RDF.install_ns(SY_PREFIX, SY_URI) module SyndicationModel - + extend BaseModel - + ELEMENTS = [] - + def self.append_features(klass) super @@ -29,21 +31,24 @@ module RSS %w(updateBase).each do |name| install_date_element(name, SY_URI, "?", - "#{SY_PREFIX}_#{name}", 'w3cdtf', name) + "#{SY_PREFIX}_#{name}", 'w3cdtf', + "#{SY_PREFIX}:#{name}") end + end + klass.module_eval(<<-EOC, __FILE__, __LINE__ + 1) alias_method(:_sy_updatePeriod=, :sy_updatePeriod=) def sy_updatePeriod=(new_value) new_value = new_value.strip validate_sy_updatePeriod(new_value) if @do_validate self._sy_updatePeriod = new_value end - end + EOC end private SY_UPDATEPERIOD_AVAILABLE_VALUES = %w(hourly daily weekly monthly yearly) - def validate_sy_updatePeriod(value) + def validate_sy_updatePeriod(value) # :nodoc: unless SY_UPDATEPERIOD_AVAILABLE_VALUES.include?(value) raise NotAvailableValueError.new("updatePeriod", value) end @@ -58,7 +63,7 @@ module RSS SyndicationModel::ELEMENTS.uniq! SyndicationModel::ELEMENTS.each do |full_name| name = full_name[prefix_size..-1] - BaseListener.install_get_text_element(SY_URI, name, "#{full_name}=") + BaseListener.install_get_text_element(SY_URI, name, full_name) end end diff --git a/lib/rss/taxonomy.rb b/lib/rss/taxonomy.rb index 8caa25e2a4..b7ea219e8c 100644 --- a/lib/rss/taxonomy.rb +++ b/lib/rss/taxonomy.rb @@ -1,18 +1,21 @@ +# frozen_string_literal: false require "rss/1.0" require "rss/dublincore" module RSS - + # The prefix for the Taxonomy XML namespace. TAXO_PREFIX = "taxo" + # The URI for the specification of the Taxonomy XML namespace. TAXO_URI = "http://purl.org/rss/1.0/modules/taxonomy/" RDF.install_ns(TAXO_PREFIX, TAXO_URI) + # The listing of all the taxonomy elements, with the appropriate namespace. TAXO_ELEMENTS = [] %w(link).each do |name| full_name = "#{TAXO_PREFIX}_#{name}" - BaseListener.install_get_text_element(TAXO_URI, name, "#{full_name}=") + BaseListener.install_get_text_element(TAXO_URI, name, full_name) TAXO_ELEMENTS << "#{TAXO_PREFIX}_#{name}" end @@ -24,7 +27,7 @@ module RSS module TaxonomyTopicsModel extend BaseModel - + def self.append_features(klass) super @@ -37,21 +40,21 @@ module RSS class TaxonomyTopics < Element include RSS10 - + Bag = ::RSS::RDF::Bag class << self def required_prefix TAXO_PREFIX end - + def required_uri TAXO_URI end end @tag_name = "topics" - + install_have_child_element("Bag", RDF::URI, nil) install_must_call_validator('rdf', RDF::URI) @@ -84,10 +87,10 @@ module RSS end end end - + module TaxonomyTopicModel extend BaseModel - + def self.append_features(klass) super var_name = "#{TAXO_PREFIX}_topic" @@ -99,12 +102,12 @@ module RSS include DublinCoreModel include TaxonomyTopicsModel - + class << self def required_prefix TAXO_PREFIX end - + def required_uri TAXO_URI end @@ -115,7 +118,7 @@ module RSS install_get_attribute("about", ::RSS::RDF::URI, true, nil, nil, "#{RDF::PREFIX}:about") install_text_element("link", TAXO_URI, "?", "#{TAXO_PREFIX}_link") - + def initialize(*args) if Utils.element_initialize_arguments?(args) super diff --git a/lib/rss/trackback.rb b/lib/rss/trackback.rb index ee2491f332..1a3c3849b5 100644 --- a/lib/rss/trackback.rb +++ b/lib/rss/trackback.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: false +# This file contains the implementation of trackbacks. It is entirely internal +# and not useful to outside developers. require 'rss/1.0' require 'rss/2.0' -module RSS +module RSS # :nodoc: all TRACKBACK_PREFIX = 'trackback' TRACKBACK_URI = 'http://madskills.com/public/xml/rss/module/trackback/' @@ -23,7 +26,7 @@ module RSS module BaseTrackBackModel ELEMENTS = %w(ping about) - + def append_features(klass) super @@ -47,7 +50,7 @@ module RSS end EOC end - + [%w(about s)].each do |name, postfix| var_name = "#{TRACKBACK_PREFIX}_#{name}" klass_name = "TrackBack#{Utils.to_class_name(name)}" @@ -105,7 +108,7 @@ module RSS def required_prefix TRACKBACK_PREFIX end - + def required_uri TRACKBACK_URI end @@ -141,17 +144,17 @@ module RSS include RSS10 class << self - + def required_prefix TRACKBACK_PREFIX end - + def required_uri TRACKBACK_URI end end - + @tag_name = "about" [ @@ -163,7 +166,7 @@ module RSS alias_method(:value, :resource) alias_method(:value=, :resource=) - + def initialize(*args) if Utils.element_initialize_arguments?(args) super @@ -185,7 +188,7 @@ module RSS def setup_maker_attributes(about) about.resource = self.resource end - + end end @@ -197,7 +200,7 @@ module RSS include RSS09 @tag_name = "ping" - + content_setup class << self @@ -205,13 +208,13 @@ module RSS def required_prefix TRACKBACK_PREFIX end - + def required_uri TRACKBACK_URI end end - + alias_method(:value, :content) alias_method(:value=, :content=) @@ -223,26 +226,26 @@ module RSS self.content = args[0] end end - + def full_name tag_name_with_prefix(TRACKBACK_PREFIX) end - + end class TrackBackAbout < Element include RSS09 @tag_name = "about" - + content_setup class << self - + def required_prefix TRACKBACK_PREFIX end - + def required_uri TRACKBACK_URI end @@ -260,11 +263,11 @@ module RSS self.content = args[0] end end - + def full_name tag_name_with_prefix(TRACKBACK_PREFIX) end - + end end diff --git a/lib/rss/utils.rb b/lib/rss/utils.rb index b242a72292..9203df9a9b 100644 --- a/lib/rss/utils.rb +++ b/lib/rss/utils.rb @@ -1,25 +1,110 @@ +# frozen_string_literal: false module RSS + + ## + # RSS::Utils is a module that holds various utility functions that are used + # across many parts of the rest of the RSS library. Like most modules named + # some variant of 'util', its methods are probably not particularly useful + # to those who aren't developing the library itself. module Utils module_function - # Convert a name_with_underscores to CamelCase. + # Given a +name+ in a name_with_underscores or a name-with-dashes format, + # returns the CamelCase version of +name+. + # + # If the +name+ is already CamelCased, nothing happens. + # + # Examples: + # + # require 'rss/utils' + # + # RSS::Utils.to_class_name("sample_name") + # # => "SampleName" + # RSS::Utils.to_class_name("with-dashes") + # # => "WithDashes" + # RSS::Utils.to_class_name("CamelCase") + # # => "CamelCase" def to_class_name(name) - name.split(/_/).collect do |part| + name.split(/[_\-]/).collect do |part| "#{part[0, 1].upcase}#{part[1..-1]}" end.join("") end - + + # Returns an array of two elements: the filename where the calling method + # is located, and the line number where it is defined. + # + # Takes an optional argument +i+, which specifies how many callers up the + # stack to look. + # + # Examples: + # + # require 'rss/utils' + # + # def foo + # p RSS::Utils.get_file_and_line_from_caller + # p RSS::Utils.get_file_and_line_from_caller(1) + # end + # + # def bar + # foo + # end + # + # def baz + # bar + # end + # + # baz + # # => ["test.rb", 5] + # # => ["test.rb", 9] + # + # If +i+ is not given, or is the default value of 0, it attempts to figure + # out the correct value. This is useful when in combination with + # instance_eval. For example: + # + # require 'rss/utils' + # + # def foo + # p RSS::Utils.get_file_and_line_from_caller(1) + # end + # + # def bar + # foo + # end + # + # instance_eval <<-RUBY, *RSS::Utils.get_file_and_line_from_caller + # def baz + # bar + # end + # RUBY + # + # baz + # + # # => ["test.rb", 8] def get_file_and_line_from_caller(i=0) file, line, = caller[i].split(':') - [file, line.to_i] + line = line.to_i + line += 1 if i.zero? + [file, line] end - # escape '&', '"', '<' and '>' for use in HTML. + # Takes a string +s+ with some HTML in it, and escapes '&', '"', '<' and '>', by + # replacing them with the appropriate entities. + # + # This method is also aliased to h, for convenience. + # + # Examples: + # + # require 'rss/utils' + # + # RSS::Utils.html_escape("Dungeons & Dragons") + # # => "Dungeons & Dragons" + # RSS::Utils.h(">_>") + # # => ">_>" def html_escape(s) s.to_s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<") end alias h html_escape - + # If +value+ is an instance of class +klass+, return it, else # create a new instance of +klass+ with value +value+. def new_with_value_if_need(klass, value) @@ -30,8 +115,86 @@ module RSS end end + # This method is used inside of several different objects to determine + # if special behavior is needed in the constructor. + # + # Special behavior is needed if the array passed in as +args+ has + # +true+ or +false+ as its value, and if the second element of +args+ + # is a hash. def element_initialize_arguments?(args) [true, false].include?(args[0]) and args[1].is_a?(Hash) end + + module ExplicitCleanOther + module_function + def parse(value) + if [true, false, nil].include?(value) + value + else + case value.to_s + when /\Aexplicit|yes|true\z/i + true + when /\Aclean|no|false\z/i + false + else + nil + end + end + end + end + + module YesOther + module_function + def parse(value) + if [true, false].include?(value) + value + else + /\Ayes\z/i.match(value.to_s) ? true : false + end + end + end + + module CSV + module_function + def parse(value, &block) + if value.is_a?(String) + value = value.strip.split(/\s*,\s*/) + value = value.collect(&block) if block_given? + value + else + value + end + end + end + + module InheritedReader + def inherited_reader(constant_name) + base_class = inherited_base + result = base_class.const_get(constant_name) + found_base_class = false + ancestors.reverse_each do |klass| + if found_base_class + if klass.const_defined?(constant_name) + result = yield(result, klass.const_get(constant_name)) + end + else + found_base_class = klass == base_class + end + end + result + end + + def inherited_array_reader(constant_name) + inherited_reader(constant_name) do |result, current| + current + result + end + end + + def inherited_hash_reader(constant_name) + inherited_reader(constant_name) do |result, current| + result.merge(current) + end + end + end end end diff --git a/lib/rss/xml-stylesheet.rb b/lib/rss/xml-stylesheet.rb index 66e3161dd0..be9cfaaf64 100644 --- a/lib/rss/xml-stylesheet.rb +++ b/lib/rss/xml-stylesheet.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false require "rss/utils" module RSS @@ -8,7 +9,7 @@ module RSS super @xml_stylesheets = [] end - + private def xml_stylesheet_pi xsss = @xml_stylesheets.collect do |xss| @@ -35,6 +36,10 @@ module RSS attr_accessor(*ATTRIBUTES) attr_accessor(:do_validate) def initialize(*attrs) + if attrs.size == 1 and + (attrs.first.is_a?(Hash) or attrs.first.is_a?(Array)) + attrs = attrs.first + end @do_validate = true ATTRIBUTES.each do |attr| __send__("#{attr}=", nil) @@ -90,7 +95,7 @@ module RSS xss.__send__("#{attr}=", __send__(attr)) end end - + private def guess_type(filename) /\.([^.]+)$/ =~ filename diff --git a/lib/rss/xml.rb b/lib/rss/xml.rb new file mode 100644 index 0000000000..cda8668044 --- /dev/null +++ b/lib/rss/xml.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: false +require "rss/utils" + +module RSS + module XML + class Element + include Enumerable + + attr_reader :name, :prefix, :uri, :attributes, :children + def initialize(name, prefix=nil, uri=nil, attributes={}, children=[]) + @name = name + @prefix = prefix + @uri = uri + @attributes = attributes + if children.is_a?(String) or !children.respond_to?(:each) + @children = [children] + else + @children = children + end + end + + def [](name) + @attributes[name] + end + + def []=(name, value) + @attributes[name] = value + end + + def <<(child) + @children << child + end + + def each(&block) + @children.each(&block) + end + + def ==(other) + other.kind_of?(self.class) and + @name == other.name and + @uri == other.uri and + @attributes == other.attributes and + @children == other.children + end + + def to_s + rv = "<#{full_name}" + attributes.each do |key, value| + rv << " #{Utils.html_escape(key)}=\"#{Utils.html_escape(value)}\"" + end + if children.empty? + rv << "/>" + else + rv << ">" + children.each do |child| + rv << child.to_s + end + rv << "</#{full_name}>" + end + rv + end + + def full_name + if @prefix + "#{@prefix}:#{@name}" + else + @name + end + end + end + end +end diff --git a/lib/rss/xmlparser.rb b/lib/rss/xmlparser.rb index 3dfe7d461a..cb2dd2afdd 100644 --- a/lib/rss/xmlparser.rb +++ b/lib/rss/xmlparser.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: false begin require "xml/parser" rescue LoadError @@ -20,15 +21,16 @@ end module XML class Parser unless defined?(Error) - Error = ::XMLParserError + # This error is legacy, so we just set it to the new one + Error = ::XMLParserError # :nodoc: end end end module RSS - + class REXMLLikeXMLParser < ::XML::Parser - + include ::XML::Encoding_ja def listener=(listener) @@ -38,7 +40,7 @@ module RSS def startElement(name, attrs) @listener.tag_start(name, attrs) end - + def endElement(name) @listener.tag_end(name) end @@ -64,7 +66,7 @@ module RSS XMLParserListener end end - + private def _parse begin @@ -75,13 +77,13 @@ module RSS raise NotWellFormedError.new(parser.line){e.message} end end - + end - + class XMLParserListener < BaseListener include ListenerMixin - + def xmldecl(version, encoding, standalone) super # Encoding is converted to UTF-8 when XMLParser parses XML. diff --git a/lib/rss/xmlscanner.rb b/lib/rss/xmlscanner.rb index 61b9fa6bf4..6e3b13d2f5 100644 --- a/lib/rss/xmlscanner.rb +++ b/lib/rss/xmlscanner.rb @@ -1,16 +1,17 @@ +# frozen_string_literal: false require 'xmlscan/scanner' require 'stringio' module RSS - + class XMLScanParser < BaseParser - + class << self def listener XMLScanListener end end - + private def _parse begin @@ -26,11 +27,11 @@ module RSS raise NotWellFormedError.new(lineno){e.message} end end - + end class XMLScanListener < BaseListener - + include XMLScan::Visitor include ListenerMixin |
