summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/rss/0.9.rb438
-rw-r--r--lib/rss/1.0.rb652
-rw-r--r--lib/rss/2.0.rb148
-rw-r--r--lib/rss/content.rb47
-rw-r--r--lib/rss/converter.rb182
-rw-r--r--lib/rss/dublincore.rb56
-rw-r--r--lib/rss/parser.rb330
-rw-r--r--lib/rss/rexmlparser.rb43
-rw-r--r--lib/rss/rss.rb536
-rw-r--r--lib/rss/syndication.rb81
-rw-r--r--lib/rss/taxonomy.rb32
-rw-r--r--lib/rss/trackback.rb235
-rw-r--r--lib/rss/utils.rb19
-rw-r--r--lib/rss/xmlparser.rb69
-rw-r--r--lib/rss/xmlscanner.rb97
15 files changed, 2965 insertions, 0 deletions
diff --git a/lib/rss/0.9.rb b/lib/rss/0.9.rb
new file mode 100644
index 0000000000..467d57ba2d
--- /dev/null
+++ b/lib/rss/0.9.rb
@@ -0,0 +1,438 @@
+require "rss/rss"
+
+module RSS
+
+ module RSS09
+ NSPOOL = {}
+ ELEMENTS = []
+ end
+
+ class Rss < Element
+
+ include RSS09
+
+ [
+ ["channel", nil],
+ ].each do |tag, occurs|
+ install_model(tag, occurs)
+ end
+
+ %w(channel).each do |x|
+ install_have_child_element(x)
+ end
+
+ attr_accessor :rss_version, :version, :encoding, :standalone
+
+ def initialize(rss_version, version=nil, encoding=nil, standalone=nil)
+ super()
+ @rss_version = rss_version
+ @version = version || '1.0'
+ @encoding = encoding
+ @standalone = standalone
+ end
+
+ def output_encoding=(enc)
+ @output_encoding = enc
+ self.converter = Converter.new(@output_encoding, @encoding)
+ end
+
+ def items
+ if @channel
+ @channel.items
+ else
+ []
+ end
+ end
+
+ def image
+ if @channel
+ @channel.image
+ else
+ nil
+ end
+ end
+
+ def to_s(convert=true)
+ rv = <<-EOR
+#{xmldecl}
+<rss version="#{@rss_version}"#{ns_declaration}>
+#{channel_element(false)}
+#{other_element(false, "\t")}
+</rss>
+EOR
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ private
+ def xmldecl
+ rv = "<?xml version='#{@version}'"
+ if @output_encoding or @encoding
+ rv << " encoding='#{@output_encoding or @encoding}'"
+ end
+ rv << " standalone='#{@standalone}'" if @standalone
+ rv << '?>'
+ rv
+ end
+
+ def ns_declaration
+ rv = ''
+ NSPOOL.each do |prefix, uri|
+ prefix = ":#{prefix}" unless prefix.empty?
+ rv << %Q|\n\txmlns#{prefix}="#{uri}"|
+ end
+ rv
+ end
+
+ def children
+ [@channel]
+ end
+
+ class Channel < Element
+
+ include RSS09
+
+ [
+ ["title", nil],
+ ["link", nil],
+ ["description", nil],
+ ["language", nil],
+ ["copyright", "?"],
+ ["managingEditor", "?"],
+ ["webMaster", "?"],
+ ["rating", "?"],
+ ["docs", "?"],
+ ["skipDays", "?"],
+ ["skipHours", "?"],
+ ].each do |x, occurs|
+ install_text_element(x)
+ install_model(x, occurs)
+ end
+
+ [
+ ["pubDate", "?"],
+ ["lastBuildDate", "?"],
+ ].each do |x, occurs|
+ install_date_element(x, 'rfc822')
+ install_model(x, occurs)
+ end
+
+ [
+ ["image", nil],
+ ["textInput", "?"],
+ ["cloud", "?"]
+ ].each do |x, occurs|
+ install_have_child_element(x)
+ install_model(x, occurs)
+ end
+
+ [
+ ["item", "*"]
+ ].each do |x, occurs|
+ install_have_children_element(x)
+ install_model(x, occurs)
+ end
+
+ def initialize()
+ super()
+ end
+
+ def to_s(convert=true)
+ rv = <<-EOT
+ <channel>
+ #{title_element(false)}
+ #{link_element(false)}
+ #{description_element(false)}
+ #{language_element(false)}
+ #{copyright_element(false)}
+ #{managingEditor_element(false)}
+ #{webMaster_element(false)}
+ #{rating_element(false)}
+ #{pubDate_element(false)}
+ #{lastBuildDate_element(false)}
+ #{docs_element(false)}
+ #{skipDays_element(false)}
+ #{skipHours_element(false)}
+ #{image_element(false)}
+#{item_elements(false)}
+ #{textInput_element(false)}
+#{other_element(false, "\t\t")}
+ </channel>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ private
+ def children
+ [@image, @textInput, @cloud, *@item]
+ end
+
+ class Image < Element
+
+ include RSS09
+
+ %w(url title link width height description).each do |x|
+ install_text_element(x)
+ end
+
+ def to_s(convert=true)
+ rv = <<-EOT
+ <image>
+ #{url_element(false)}
+ #{title_element(false)}
+ #{link_element(false)}
+ #{width_element(false)}
+ #{height_element(false)}
+ #{description_element(false)}
+#{other_element(false, "\t\t\t\t")}
+ </image>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ end
+
+ class Cloud < Element
+
+ include RSS09
+
+ [
+ ["domain", nil, false],
+ ["port", nil, false],
+ ["path", nil, false],
+ ["registerProcedure", nil, false],
+ ["protocol", nil ,false],
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ def to_s(convert=true)
+ rv = <<-EOT
+ <cloud
+ domain="#{h @domain}"
+ port="#{h @port}"
+ path="#{h @path}"
+ registerProcedure="#{h @registerProcedure}"
+ protocol="#{h @protocol}"/>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ end
+
+ class Item < Element
+
+ include RSS09
+
+ %w(title link description author comments).each do |x|
+ install_text_element(x)
+ end
+
+ %w(category source enclosure).each do |x|
+ install_have_child_element(x)
+ end
+
+ [
+ ["title", '?'],
+ ["link", '?'],
+ ["description", '?'],
+ ["author", '?'],
+ ["comments", '?'],
+ ["category", '?'],
+ ["source", '?'],
+ ["enclosure", '?'],
+ ].each do |tag, occurs|
+ install_model(tag, occurs)
+ end
+
+ def to_s(convert=true)
+ rv = <<-EOT
+ <item>
+ #{title_element(false)}
+ #{link_element(false)}
+ #{description_element(false)}
+ #{author_element(false)}
+ #{category_element(false)}
+ #{comments_element(false)}
+ #{enclosure_element(false)}
+ #{source_element(false)}
+#{other_element(false, "\t\t\t\t")}
+ </item>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ class Source < Element
+
+ include RSS09
+
+ [
+ ["url", nil, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ content_setup
+
+ def initialize(url=nil, content=nil)
+ super()
+ @url = url
+ @content = content
+ end
+
+ def to_s(convert=true)
+ if @url
+ rv = %Q! <source url="#{@url}">!
+ rv << %Q!#{@content}</source>!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["url", true]
+ ]
+ end
+
+ end
+
+ class Enclosure < Element
+
+ include RSS09
+
+ [
+ ["url", nil, true],
+ ["length", nil, true],
+ ["type", nil, true],
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ def initialize(url=nil, length=nil, type=nil)
+ super()
+ @url = url
+ @length = length
+ @type = type
+ end
+
+ def to_s(convert=true)
+ if @url and @length and @type
+ rv = %Q!<enclosure url="#{h @url}" !
+ rv << %Q!length="#{h @length}" type="#{h @type}"/>!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["url", true],
+ ["length", true],
+ ["type", true],
+ ]
+ end
+
+ end
+
+ class Category < Element
+
+ include RSS09
+
+ [
+ ["domain", nil, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ content_setup
+
+ def initialize(domain=nil, content=nil)
+ super()
+ @domain = domain
+ @content = content
+ end
+
+ def to_s(convert=true)
+ if @domain
+ rv = %Q!<category domain="#{h @domain}">!
+ rv << %Q!#{h @content}</category>!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["domain", true]
+ ]
+ end
+
+ end
+
+ end
+
+ class TextInput < Element
+
+ include RSS09
+
+ %w(title description name link).each do |x|
+ install_text_element(x)
+ end
+
+ def to_s(convert=true)
+ rv = <<-EOT
+ <textInput>
+ #{title_element(false)}
+ #{description_element(false)}
+ #{name_element(false)}
+ #{link_element(false)}
+#{other_element(false, "\t\t\t\t")}
+ </textInput>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ end
+
+ end
+
+ end
+
+ if const_defined?(:BaseListener)
+ RSS09::ELEMENTS.each do |x|
+ BaseListener.install_get_text_element(x, nil, "#{x}=")
+ end
+ end
+
+ if const_defined?(:ListenerMixin)
+ module ListenerMixin
+ private
+ def start_rss(tag_name, prefix, attrs, ns)
+ check_ns(tag_name, prefix, ns, nil)
+
+ @rss = Rss.new(attrs['version'], @version, @encoding, @standalone)
+ @last_element = @rss
+ @proc_stack.push Proc.new { |text, tags|
+ @rss.validate_for_stream(tags) if @do_validate
+ }
+ end
+
+ end
+ end
+
+end
diff --git a/lib/rss/1.0.rb b/lib/rss/1.0.rb
new file mode 100644
index 0000000000..72b7637122
--- /dev/null
+++ b/lib/rss/1.0.rb
@@ -0,0 +1,652 @@
+require "rss/rss"
+
+module RSS
+
+ module RSS10
+ NSPOOL = {}
+ ELEMENTS = []
+ end
+
+ class RDF < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ URI
+ end
+
+ end
+
+ TAG_NAME.replace('RDF')
+
+ PREFIX = 'rdf'
+ URI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+
+ install_ns('', ::RSS::URI)
+ install_ns(PREFIX, URI)
+
+ [
+ ["channel", nil],
+ ["image", "?"],
+ ["item", "+"],
+ ["textinput", "?"],
+ ].each do |tag, occurs|
+ install_model(tag, occurs)
+ end
+
+ %w(channel image textinput).each do |x|
+ install_have_child_element(x)
+ end
+
+ install_have_children_element("item")
+
+ attr_accessor :rss_version, :version, :encoding, :standalone
+
+ def initialize(version=nil, encoding=nil, standalone=nil)
+ super()
+ @rss_version = '1.0'
+ @version = version || '1.0'
+ @encoding = encoding
+ @standalone = standalone
+ @output_encoding = nil
+ end
+
+ def output_encoding=(enc)
+ @output_encoding = enc
+ self.converter = Converter.new(@output_encoding, @encoding)
+ end
+
+ def to_s(convert=true)
+ rv = <<-EORDF
+#{xmldecl}
+<#{PREFIX}:RDF#{ns_declaration}>
+#{channel_element(false)}
+#{image_element(false)}
+#{item_elements(false)}
+#{textinput_element(false)}
+#{other_element(false, "\t")}
+</#{PREFIX}:RDF>
+EORDF
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ private
+ def xmldecl
+ rv = "<?xml version='#{@version}'"
+ if @output_encoding or @encoding
+ rv << " encoding='#{@output_encoding or @encoding}'"
+ end
+ rv << " standalone='#{@standalone}'" if @standalone
+ rv << '?>'
+ rv
+ end
+
+ def ns_declaration
+ rv = ''
+ self.class::NSPOOL.each do |prefix, uri|
+ prefix = ":#{prefix}" unless prefix.empty?
+ rv << %Q|\n\txmlns#{prefix}="#{html_escape(uri)}"|
+ end
+ rv
+ end
+
+ def rdf_validate(tags)
+ _validate(tags, [])
+ end
+
+ def children
+ [@channel, @image, @textinput, *@item]
+ end
+
+ def _tags
+ rv = [
+ [::RSS::URI, "channel"],
+ [::RSS::URI, "image"],
+ ].delete_if {|x| send(x[1]).nil?}
+ @item.each do |x|
+ rv << [::RSS::URI, "item"]
+ end
+ rv << [::RSS::URI, "textinput"] if @textinput
+ rv
+ end
+
+ class Seq < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ URI
+ end
+
+ end
+
+ TAG_NAME.replace('Seq')
+
+ install_have_children_element("li")
+
+ install_must_call_validator('rdf', ::RSS::RDF::URI)
+
+ def initialize(li=[])
+ super()
+ @li = li
+ end
+
+ def to_s(convert=true)
+ <<-EOT
+ <#{PREFIX}:Seq>
+#{li_elements(convert, "\t\t\t\t\t")}
+#{other_element(convert, "\t\t\t\t\t")}
+ </#{PREFIX}:Seq>
+EOT
+ end
+
+ private
+ def children
+ @li
+ end
+
+ def rdf_validate(tags)
+ _validate(tags, [["li", '*']])
+ end
+
+ def _tags
+ rv = []
+ @li.each do |x|
+ rv << [URI, "li"]
+ end
+ rv
+ end
+
+ end
+
+ class Li < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ URI
+ end
+
+ end
+
+ [
+ ["resource", nil, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ def initialize(resource=nil)
+ super()
+ @resource = resource
+ end
+
+ def to_s(convert=true)
+ if @resource
+ rv = %Q!<#{PREFIX}:li resource="#{h @resource}" />!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["resource", true]
+ ]
+ end
+
+ end
+
+ class Channel < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ ::RSS::URI
+ end
+
+ end
+
+ [
+ ["about", URI, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ %w(title link description).each do |x|
+ install_text_element(x)
+ end
+
+ %w(image items textinput).each do |x|
+ install_have_child_element(x)
+ end
+
+ [
+ ['title', nil],
+ ['link', nil],
+ ['description', nil],
+ ['image', '?'],
+ ['items', nil],
+ ['textinput', '?'],
+ ].each do |tag, occurs|
+ install_model(tag, occurs)
+ end
+
+ def initialize(about=nil)
+ super()
+ @about = about
+ end
+
+ def to_s(convert=true)
+ about = ''
+ about << %Q!#{PREFIX}:about="#{h @about}"! if @about
+ rv = <<-EOT
+ <channel #{about}>
+ #{title_element(false)}
+ #{link_element(false)}
+ #{description_element(false)}
+ #{image_element(false)}
+#{items_element(false)}
+ #{textinput_element(false)}
+#{other_element(false, "\t\t")}
+ </channel>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ private
+ def children
+ [@image, @items, @textinput]
+ end
+
+ def _tags
+ [
+ [::RSS::URI, 'title'],
+ [::RSS::URI, 'link'],
+ [::RSS::URI, 'description'],
+ [::RSS::URI, 'image'],
+ [::RSS::URI, 'items'],
+ [::RSS::URI, 'textinput'],
+ ].delete_if do |x|
+ send(x[1]).nil?
+ end
+ end
+
+ def _attrs
+ [
+ ["about", true]
+ ]
+ end
+
+ class Image < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ ::RSS::URI
+ end
+
+ end
+
+ [
+ ["resource", URI, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ def initialize(resource=nil)
+ super()
+ @resource = resource
+ end
+
+ def to_s(convert=true)
+ if @resource
+ rv = %Q!<image #{PREFIX}:resource="#{h @resource}" />!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["resource", true]
+ ]
+ end
+
+ end
+
+ class Textinput < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ ::RSS::URI
+ end
+
+ end
+
+ [
+ ["resource", URI, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ def initialize(resource=nil)
+ super()
+ @resource = resource
+ end
+
+ def to_s(convert=true)
+ if @resource
+ rv = %Q|<textinput #{PREFIX}:resource="#{h @resource}" />|
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["resource", true],
+ ]
+ end
+
+ end
+
+ class Items < Element
+
+ include RSS10
+
+ Seq = ::RSS::RDF::Seq
+ class Seq
+ unless const_defined?(:Li)
+ Li = ::RSS::RDF::Li
+ end
+ end
+
+ class << self
+
+ def required_uri
+ ::RSS::URI
+ end
+
+ end
+
+ install_have_child_element("Seq")
+
+ install_must_call_validator('rdf', ::RSS::RDF::URI)
+
+ def initialize(seq=Seq.new)
+ super()
+ @Seq = seq
+ end
+
+ def to_s(convert=true)
+ <<-EOT
+ <items>
+#{Seq_element(convert)}
+#{other_element(convert, "\t\t\t")}
+ </items>
+EOT
+ end
+
+ private
+ def children
+ [@Seq]
+ end
+
+ private
+ def _tags
+ rv = []
+ rv << [URI, 'Seq'] unless @Seq.nil?
+ rv
+ end
+
+ def rdf_validate(tags)
+ _validate(tags, [["Seq", nil]])
+ end
+
+ end
+
+ end
+
+ class Image < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ ::RSS::URI
+ end
+
+ end
+
+ [
+ ["about", URI, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ %w(title url link).each do |x|
+ install_text_element(x)
+ end
+
+ [
+ ['title', nil],
+ ['url', nil],
+ ['link', nil],
+ ].each do |tag, occurs|
+ install_model(tag, occurs)
+ end
+
+ def initialize(about=nil)
+ super()
+ @about = about
+ end
+
+ def to_s(convert=true)
+ about = ''
+ about << %Q!#{PREFIX}:about="#{h @about}"! if @about
+ rv = <<-EOT
+ <image #{about}>
+ #{title_element(false)}
+ #{url_element(false)}
+ #{link_element(false)}
+#{other_element(false, "\t\t")}
+ </image>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ private
+ def _tags
+ [
+ [::RSS::URI, 'title'],
+ [::RSS::URI, 'url'],
+ [::RSS::URI, 'link'],
+ ].delete_if do |x|
+ send(x[1]).nil?
+ end
+ end
+
+ def _attrs
+ [
+ ["about", true],
+ ]
+ end
+
+ end
+
+ class Item < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ ::RSS::URI
+ end
+
+ end
+
+ [
+ ["about", URI, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ %w(title link description).each do |x|
+ install_text_element(x)
+ end
+
+ [
+ ["title", nil],
+ ["link", nil],
+ ["description", "?"],
+ ].each do |tag, occurs|
+ install_model(tag, occurs)
+ end
+
+ def initialize(about=nil)
+ super()
+ @about = about
+ end
+
+ def to_s(convert=true)
+ about = ''
+ about << %Q!#{PREFIX}:about="#{h @about}"! if @about
+ rv = <<-EOT
+ <item #{about}>
+ #{title_element(false)}
+ #{link_element(false)}
+ #{description_element(false)}
+#{other_element(false, "\t\t")}
+ </item>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ private
+ def _tags
+ [
+ [::RSS::URI, 'title'],
+ [::RSS::URI, 'link'],
+ [::RSS::URI, 'description'],
+ ].delete_if do |x|
+ send(x[1]).nil?
+ end
+ end
+
+ def _attrs
+ [
+ ["about", true],
+ ]
+ end
+
+ end
+
+ class Textinput < Element
+
+ include RSS10
+
+ class << self
+
+ def required_uri
+ ::RSS::URI
+ end
+
+ end
+
+ [
+ ["about", URI, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ %w(title description name link).each do |x|
+ install_text_element(x)
+ end
+
+ [
+ ["title", nil],
+ ["description", nil],
+ ["name", nil],
+ ["link", nil],
+ ].each do |tag, occurs|
+ install_model(tag, occurs)
+ end
+
+ def initialize(about=nil)
+ super()
+ @about = about
+ end
+
+ def to_s(convert=true)
+ about = ''
+ about << %Q!#{PREFIX}:about="#{h @about}"! if @about
+ rv = <<-EOT
+ <textinput #{about}>
+ #{title_element(false)}
+ #{description_element(false)}
+ #{name_element(false)}
+ #{link_element(false)}
+#{other_element(false, "\t\t")}
+ </textinput>
+EOT
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ end
+
+ private
+ def _tags
+ [
+ [::RSS::URI, 'title'],
+ [::RSS::URI, 'description'],
+ [::RSS::URI, 'name'],
+ [::RSS::URI, 'link'],
+ ].delete_if do |x|
+ send(x[1]).nil?
+ end
+ end
+
+ def _attrs
+ [
+ ["about", true],
+ ]
+ end
+
+ end
+
+ end
+
+ if const_defined?(:BaseListener)
+ RSS10::ELEMENTS.each do |x|
+ BaseListener.install_get_text_element(x, URI, "#{x}=")
+ end
+ end
+
+end
diff --git a/lib/rss/2.0.rb b/lib/rss/2.0.rb
new file mode 100644
index 0000000000..e43947e400
--- /dev/null
+++ b/lib/rss/2.0.rb
@@ -0,0 +1,148 @@
+require "rss/0.9"
+
+module RSS
+
+ class Rss
+
+ URI = "http://backend.userland.com/rss2"
+
+ install_ns('', URI)
+
+ def self.required_uri
+ URI
+ end
+
+ class Channel
+
+ def self.required_uri
+ URI
+ end
+
+ %w(generator ttl).each do |x|
+ install_text_element(x)
+ end
+
+ %w(category).each do |x|
+ install_have_child_element(x)
+ end
+
+ [
+ ["image", "?"],
+ ].each do |x, occurs|
+ install_model(x, occurs)
+ end
+
+ def other_element(convert, indent='')
+ rv = <<-EOT
+#{indent}#{category_element(convert)}
+#{indent}#{generator_element(convert)}
+#{indent}#{ttl_element(convert)}
+EOT
+ rv << super
+ end
+
+ Category = Item::Category
+ def Category.required_uri
+ URI
+ end
+
+ class Item
+
+ def self.required_uri
+ URI
+ end
+
+ [
+ ["pubDate", '?'],
+ ].each do |x, occurs|
+ install_date_element(x, 'rfc822')
+ install_model(x, occurs)
+ end
+
+ [
+ ["guid", '?'],
+ ].each do |x, occurs|
+ install_have_child_element(x)
+ install_model(x, occurs)
+ end
+
+ def other_element(convert, indent='')
+ rv = <<-EOT
+#{indent}#{pubDate_element(false)}
+#{indent}#{guid_element(false)}
+EOT
+ rv << super
+ end
+
+ class Guid < Element
+
+ include RSS09
+
+ def self.required_uri
+ URI
+ end
+
+ [
+ ["isPermaLink", nil, false]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ content_setup
+
+ def initialize(isPermaLink=nil, content=nil)
+ super()
+ @isPermaLink = isPermaLink
+ @content = content
+ end
+
+ def to_s(convert=true)
+ if @content
+ rv = %Q!<guid!
+ rv << %Q! isPermaLink="#{h @isPermaLink}"! if @isPermaLink
+ rv << %Q!>#{h @content}</guid>!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["isPermaLink", false]
+ ]
+ end
+
+ end
+
+ end
+
+ end
+
+ end
+
+ if const_defined?(:BaseListener)
+ RSS09::ELEMENTS.each do |x|
+ BaseListener.install_get_text_element(x, Rss::URI, "#{x}=")
+ end
+ end
+
+ if const_defined?(:ListenerMixin)
+ module ListenerMixin
+ private
+ def start_rss(tag_name, prefix, attrs, ns)
+ check_ns(tag_name, prefix, ns, Rss::URI)
+
+ @rss = Rss.new(attrs['version'], @version, @encoding, @standalone)
+ @last_element = @rss
+ @proc_stack.push Proc.new { |text, tags|
+ @rss.validate_for_stream(tags) if @do_validate
+ }
+ end
+
+ end
+ end
+
+end
diff --git a/lib/rss/content.rb b/lib/rss/content.rb
new file mode 100644
index 0000000000..64f87d4d5b
--- /dev/null
+++ b/lib/rss/content.rb
@@ -0,0 +1,47 @@
+require "rss/1.0"
+
+module RSS
+
+ CONTENT_PREFIX = 'content'
+ CONTENT_URI = "http://purl.org/rss/1.0/modules/content/"
+
+ RDF.install_ns(CONTENT_PREFIX, CONTENT_URI)
+
+ module ContentModel
+
+ extend BaseModel
+
+ ELEMENTS = []
+
+ %w(encoded).each do |x|
+ install_text_element("#{CONTENT_PREFIX}_#{x}")
+ end
+
+ def content_validate(tags)
+ counter = {}
+ ELEMENTS.each do |x|
+ counter[x] = 0
+ end
+
+ tags.each do |tag|
+ key = "#{CONTENT_PREFIX}_#{tag}"
+ raise UnknownTagError.new(tag, CONTENT_URI) unless counter.has_key?(key)
+ counter[key] += 1
+ raise TooMuchTagError.new(tag, tag_name) if counter[key] > 1
+ end
+ end
+
+ end
+
+ class RDF
+ class Item; include ContentModel; end
+ end
+
+ if const_defined? :BaseListener
+ prefix_size = CONTENT_PREFIX.size + 1
+ ContentModel::ELEMENTS.each do |x|
+ BaseListener.install_get_text_element(x[prefix_size..-1], CONTENT_URI, "#{x}=")
+ end
+ end
+
+end
diff --git a/lib/rss/converter.rb b/lib/rss/converter.rb
new file mode 100644
index 0000000000..144daab564
--- /dev/null
+++ b/lib/rss/converter.rb
@@ -0,0 +1,182 @@
+require "rss/utils"
+
+module RSS
+
+ class Converter
+
+ include Utils
+
+ def initialize(to_enc, from_enc=nil)
+ to_enc = to_enc.downcase.gsub(/-/, '_')
+ from_enc ||= 'utf-8'
+ from_enc = from_enc.downcase.gsub(/-/, '_')
+ if to_enc == from_enc
+ def_same_enc()
+ else
+ if respond_to?("def_to_#{to_enc}_from_#{from_enc}")
+ send("def_to_#{to_enc}_from_#{from_enc}")
+ else
+ def_else_enc(to_enc, from_enc)
+ end
+ end
+ end
+
+ def convert(value)
+ value
+ end
+
+ def def_convert()
+ instance_eval(<<-EOC, *get_file_and_line_from_caller(0))
+ def convert(value)
+ if value.kind_of?(String)
+ #{yield('value')}
+ else
+ value
+ end
+ end
+ EOC
+ end
+
+ def def_iconv_convert(to_enc, from_enc)
+ begin
+ require "iconv"
+ def_convert do |value|
+ <<-EOC
+ @iconv ||= Iconv.new("#{to_enc}", "#{from_enc}")
+ begin
+ @iconv.iconv(#{value})
+ rescue Iconv::Failure
+ raise ConversionError.new(#{value}, "#{to_enc}", "#{from_enc}")
+ #{value}
+ end
+EOC
+ end
+ rescue LoadError, ArgumentError, SystemCallError
+ raise UnknownConversionMethodError.new(to_enc, from_enc)
+ end
+ end
+
+ def def_else_enc(to_enc, from_enc)
+ raise UnknownConversionMethodError.new(to_enc, from_enc)
+ end
+
+ def def_same_enc()
+ def_convert do |value|
+ value
+ end
+ end
+
+ def def_to_euc_jp_from_utf_8
+ begin
+ require "uconv"
+ def_convert do |value|
+ "Uconv.u8toeuc(#{value})"
+ end
+ rescue LoadError
+ def_iconv_convert('EUC-JP', 'UTF-8')
+ end
+ end
+
+ def def_to_utf_8_from_euc_jp
+ begin
+ require "uconv"
+ def_convert do |value|
+ "Uconv.euctou8(#{value})"
+ end
+ rescue LoadError
+ def_iconv_convert('UTF-8', 'EUC-JP')
+ end
+ end
+
+ def def_to_shift_jis_from_utf_8
+ begin
+ require "uconv"
+ def_convert do |value|
+ "Uconv.u8tosjis(#{value})"
+ end
+ rescue LoadError
+ def_iconv_convert('Shift_JIS', 'UTF-8')
+ end
+ end
+
+ def def_to_utf_8_from_shift_jis
+ begin
+ require "uconv"
+ def_convert do |value|
+ "Uconv.sjistou8(#{value})"
+ end
+ rescue LoadError
+ def_iconv_convert('UTF-8', 'Shift_JIS')
+ end
+ end
+
+ def def_to_euc_jp_from_shift_jis
+ begin
+ require "nkf"
+ rescue LoadError
+ raise UnknownConversionMethodError.new('EUC-JP', 'Shift_JIS')
+ end
+ def_convert do |value|
+ "NKF.nkf('-Se', #{value})"
+ end
+ end
+
+ def def_to_shift_jis_from_euc_jp
+ begin
+ require "nkf"
+ rescue LoadError
+ raise UnknownConversionMethodError.new('Shift_JIS', 'EUC-JP')
+ end
+ def_convert do |value|
+ "NKF.nkf('-Es', #{value})"
+ end
+ end
+
+ def def_to_euc_jp_from_iso_2022_jp
+ begin
+ require "nkf"
+ rescue LoadError
+ raise UnknownConversionMethodError.new('EUC-JP', 'ISO-2022-JP')
+ end
+ def_convert do |value|
+ "NKF.nkf('-Je', #{value})"
+ end
+ end
+
+ def def_to_iso_2022_jp_from_euc_jp
+ begin
+ require "nkf"
+ rescue LoadError
+ raise UnknownConversionMethodError.new('ISO-2022-JP', 'EUC-JP')
+ end
+ def_convert do |value|
+ "NKF.nkf('-Ej', #{value})"
+ end
+ end
+
+ def def_to_utf_8_from_iso_8859_1
+ def_convert do |value|
+ "#{value}.unpack('C*').pack('U*')"
+ end
+ end
+
+ def def_to_iso_8859_1_from_utf_8
+ def_convert do |value|
+ <<-EOC
+ array_utf8 = #{value}.unpack('U*')
+ array_enc = []
+ array_utf8.each do |num|
+ if num <= 0xFF
+ array_enc << num
+ else
+ array_enc.concat "&\#\#{num};".unpack('C*')
+ end
+ end
+ array_enc.pack('C*')
+EOC
+ end
+ end
+
+ end
+
+end
diff --git a/lib/rss/dublincore.rb b/lib/rss/dublincore.rb
new file mode 100644
index 0000000000..e9c3fcac2d
--- /dev/null
+++ b/lib/rss/dublincore.rb
@@ -0,0 +1,56 @@
+require "rss/1.0"
+
+module RSS
+
+ DC_PREFIX = 'dc'
+ DC_URI = "http://purl.org/dc/elements/1.1/"
+
+ RDF.install_ns(DC_PREFIX, DC_URI)
+
+ module DublincoreModel
+
+ extend BaseModel
+
+ ELEMENTS = []
+
+ %w(title description creator subject publisher
+ contributor type format identifier source
+ language relation coverage rights).each do |x|
+ install_text_element("#{DC_PREFIX}_#{x}")
+ end
+
+ %w(date).each do |x|
+ install_date_element("#{DC_PREFIX}_#{x}", 'iso8601', x)
+ end
+
+ def dc_validate(tags)
+ counter = {}
+ ELEMENTS.each do |x|
+ counter[x] = 0
+ end
+
+ tags.each do |tag|
+ key = "#{DC_PREFIX}_#{tag}"
+ raise UnknownTagError.new(tag, DC_URI) unless counter.has_key?(key)
+ counter[key] += 1
+ raise TooMuchTagError.new(tag, tag_name) if counter[key] > 1
+ end
+ end
+
+ end
+
+ class RDF
+ class Channel; include DublincoreModel; end
+ class Image; include DublincoreModel; end
+ class Item; include DublincoreModel; end
+ class Textinput; include DublincoreModel; end
+ end
+
+ if const_defined? :BaseListener
+ prefix_size = DC_PREFIX.size + 1
+ DublincoreModel::ELEMENTS.each do |x|
+ BaseListener.install_get_text_element(x[prefix_size..-1], DC_URI, "#{x}=")
+ end
+ end
+
+end
diff --git a/lib/rss/parser.rb b/lib/rss/parser.rb
new file mode 100644
index 0000000000..39ccdfbc36
--- /dev/null
+++ b/lib/rss/parser.rb
@@ -0,0 +1,330 @@
+require "rss/rss"
+
+module RSS
+
+ class NotWellFormedError < Error
+ attr_reader :line, :element
+ def initialize(line=nil, element=nil)
+ message = "This is not well formed XML"
+ if element or line
+ message << "\nerror occurred"
+ message << " in #{element}" if element
+ message << " at about #{line} line" if line
+ end
+ message << "\n#{yield}" if block_given?
+ super(message)
+ end
+ end
+
+ class XMLParserNotFound < Error
+ def initialize
+ super("available XML parser does not found in " <<
+ "#{AVAILABLE_PARSERS.inspect}.")
+ end
+ end
+
+ class NSError < InvalidRSSError
+ attr_reader :tag, :prefix, :uri
+ def initialize(tag, prefix, require_uri)
+ @tag, @prefix, @uri = tag, prefix, require_uri
+ super("prefix <#{prefix}> doesn't associate uri " <<
+ "<#{require_uri}> in tag <#{tag}>")
+ end
+ end
+
+ class BaseParser
+
+ def initialize(rss)
+ @listener = Listener.new
+ @rss = rss
+ end
+
+ def rss
+ @listener.rss
+ end
+
+ def ignore_unknown_element
+ @listener.ignore_unknown_element
+ end
+
+ def ignore_unknown_element=(new_value)
+ @listener.ignore_unknown_element = new_value
+ end
+
+ def do_validate
+ @listener.do_validate
+ end
+
+ def do_validate=(new_value)
+ @listener.do_validate = new_value
+ end
+
+ def parse
+ if @listener.rss.nil?
+ _parse
+ end
+ @listener.rss
+ end
+
+ class << self
+ def parse(rss, do_validate=true, ignore_unknown_element=true)
+ parser = new(rss)
+ parser.do_validate = do_validate
+ parser.ignore_unknown_element = ignore_unknown_element
+ parser.parse
+ end
+ end
+
+ end
+
+ class BaseListener
+
+ extend Utils
+
+ class << self
+
+ @@setter = {}
+ def install_setter(uri, tag_name, setter)
+ @@setter[uri] = {} unless @@setter.has_key?(uri)
+ @@setter[uri][tag_name] = setter
+ end
+
+ def setter(uri, tag_name)
+ begin
+ @@setter[uri][tag_name]
+ rescue NameError
+ nil
+ end
+ end
+
+ def available_tags(uri)
+ begin
+ @@setter[uri].keys
+ rescue NameError
+ []
+ end
+ end
+
+ def install_get_text_element(name, uri, setter)
+ install_setter(uri, name, setter)
+ def_get_text_element(name, *get_file_and_line_from_caller(1))
+ end
+
+ private
+
+ def def_get_text_element(name, file, line)
+ unless private_instance_methods(false).include?("start_#{name}")
+ module_eval(<<-EOT, file, line)
+ def start_#{name}(name, prefix, attrs, ns)
+ uri = ns[prefix]
+ if @do_validate
+ tags = self.class.available_tags(uri)
+ unless tags.include?(name)
+ raise UnknownTagError.new(name, uri)
+ end
+ end
+ start_get_text_element(name, prefix, ns, uri)
+ end
+ EOT
+ end
+ send("private", "start_#{name}")
+ end
+
+ end
+
+ end
+
+ module ListenerMixin
+
+ attr_reader :rss
+
+ attr_accessor :ignore_unknown_element
+ attr_accessor :do_validate
+
+ def initialize
+ @rss = nil
+ @ignore_unknown_element = true
+ @do_validate = true
+ @ns_stack = [{}]
+ @tag_stack = [[]]
+ @text_stack = ['']
+ @proc_stack = []
+ @last_element = nil
+ @version = @encoding = @standalone = nil
+ end
+
+ def xmldecl(version, encoding, standalone)
+ @version, @encoding, @standalone = version, encoding, standalone
+ end
+
+ def tag_start(name, attributes)
+ @text_stack.push('')
+
+ ns = @ns_stack.last.dup
+ attrs = {}
+ attributes.each do |n, v|
+ if n =~ /\Axmlns:?/
+ ns[$'] = v # $' is post match
+ else
+ attrs[n] = v
+ end
+ end
+ @ns_stack.push(ns)
+
+ prefix, local = split_name(name)
+ @tag_stack.last.push([ns[prefix], local])
+ @tag_stack.push([])
+ if respond_to?("start_#{local}", true)
+ send("start_#{local}", local, prefix, attrs, ns.dup)
+ else
+ start_else_element(local, prefix, attrs, ns.dup)
+ end
+ end
+
+ def tag_end(name)
+ if $DEBUG
+ p "end tag #{name}"
+ p @tag_stack
+ end
+ text = @text_stack.pop
+ tags = @tag_stack.pop
+ pr = @proc_stack.pop
+ pr.call(text, tags) unless pr.nil?
+ end
+
+ def text(data)
+ @text_stack.last << data
+ end
+
+ private
+
+ def start_RDF(tag_name, prefix, attrs, ns)
+ check_ns(tag_name, prefix, ns, RDF::URI)
+
+ @rss = RDF.new(@version, @encoding, @standalone)
+ @rss.do_validate = @do_validate
+ @last_element = @rss
+ @proc_stack.push Proc.new { |text, tags|
+ @rss.validate_for_stream(tags) if @do_validate
+ }
+ end
+
+ def start_else_element(local, prefix, attrs, ns)
+ class_name = local[0,1].upcase << local[1..-1]
+ current_class = @last_element.class
+ begin
+# if current_class.const_defined?(class_name)
+ next_class = current_class.const_get(class_name)
+ start_have_something_element(local, prefix, attrs, ns, next_class)
+ rescue NameError
+# else
+ if @ignore_unknown_element
+ @proc_stack.push(nil)
+ else
+ parent = "ROOT ELEMENT???"
+ begin
+ parent = current_class::TAG_NAME
+ rescue NameError
+ end
+ raise NotExceptedTagError.new(local, parent)
+ end
+ end
+ end
+
+ NAMESPLIT = /^(?:([\w:][-\w\d.]*):)?([\w:][-\w\d.]*)/
+ def split_name(name)
+ name =~ NAMESPLIT
+ [$1 || '', $2]
+ end
+
+ def check_ns(tag_name, prefix, ns, require_uri)
+ if @do_validate
+ if ns[prefix] == require_uri
+ #ns.delete(prefix)
+ else
+ raise NSError.new(tag_name, prefix, require_uri)
+ end
+ end
+ end
+
+ def start_get_text_element(tag_name, prefix, ns, required_uri)
+ @proc_stack.push Proc.new {|text, tags|
+ setter = self.class.setter(required_uri, tag_name)
+ setter ||= "#{tag_name}="
+ if @last_element.respond_to?(setter)
+ @last_element.send(setter, text.to_s)
+ else
+ if @do_validate and not @ignore_unknown_element
+ raise NotExceptedTagError.new(tag_name, @last_element.tag_name)
+ end
+ end
+ }
+ end
+
+ def start_have_something_element(tag_name, prefix, attrs, ns, klass)
+
+ check_ns(tag_name, prefix, ns, klass.required_uri)
+
+ args = []
+
+ klass.get_attributes.each do |a_name, a_uri, required|
+
+ if a_uri
+ for prefix, uri in ns
+ if uri == a_uri
+ val = attrs["#{prefix}:#{a_name}"]
+ break if val
+ end
+ end
+ else
+ val = attrs[a_name]
+ end
+
+ if @do_validate and required and val.nil?
+ raise MissingAttributeError.new(tag_name, a_name)
+ end
+
+ args << val
+ end
+
+ previous = @last_element
+ next_element = klass.send(:new, *args)
+ next_element.do_validate = @do_validate
+ setter = ""
+ setter << "#{klass.required_prefix}_" if klass.required_prefix
+ setter << "#{tag_name}="
+ @last_element.send(setter, next_element)
+ @last_element = next_element
+ @proc_stack.push Proc.new { |text, tags|
+ p @last_element.class if $DEBUG
+ @last_element.content = text if klass.have_content?
+ @last_element.validate_for_stream(tags) if @do_validate
+ @last_element = previous
+ }
+ end
+
+ end
+
+ unless const_defined? :AVAILABLE_PARSERS
+ AVAILABLE_PARSERS = [
+ "rss/xmlparser",
+ "rss/xmlscanner",
+ "rss/rexmlparser",
+ ]
+ end
+
+ loaded = false
+ AVAILABLE_PARSERS.each do |parser|
+ begin
+ require parser
+ loaded = true
+ break
+ rescue LoadError
+ end
+ end
+
+ unless loaded
+ raise XMLParserNotFound
+ end
+end
+
diff --git a/lib/rss/rexmlparser.rb b/lib/rss/rexmlparser.rb
new file mode 100644
index 0000000000..b3d801597a
--- /dev/null
+++ b/lib/rss/rexmlparser.rb
@@ -0,0 +1,43 @@
+require "rexml/document"
+require "rexml/streamlistener"
+
+/\A(\d+)\.(\d+).\d+\z/ =~ REXML::Version
+if $1.to_i < 2 or ($1.to_i == 2 and $2.to_i < 5)
+ raise LoadError
+end
+
+module RSS
+
+ class Parser < BaseParser
+
+ private
+ def _parse
+ begin
+ REXML::Document.parse_stream(@rss, @listener)
+ rescue RuntimeError => e
+ raise NotWellFormedError.new{e.message}
+ rescue REXML::ParseException => e
+ context = e.context
+ line = context[0] if context
+ raise NotWellFormedError.new(line){e.message}
+ end
+ end
+
+ end
+
+ class Listener < BaseListener
+
+ include REXML::StreamListener
+ include ListenerMixin
+
+
+ def xmldecl(version, encoding, standalone)
+ super
+ # Encoding is converted to UTF-8 when REXML parse XML.
+ @encoding = 'UTF-8'
+ end
+
+ alias_method(:cdata, :text)
+ end
+
+end
diff --git a/lib/rss/rss.rb b/lib/rss/rss.rb
new file mode 100644
index 0000000000..0acf7803e5
--- /dev/null
+++ b/lib/rss/rss.rb
@@ -0,0 +1,536 @@
+require "time"
+
+require "rss/utils"
+require "rss/converter"
+
+module RSS
+
+ VERSION = "0.0.7"
+
+ class Error < StandardError; end
+
+ class OverlappedPrefixError < Error
+ attr_reader :prefix
+ def initialize(prefix)
+ @prefix = prefix
+ end
+ end
+
+ class InvalidRSSError < Error; end
+
+ class MissingTagError < InvalidRSSError
+ attr_reader :tag, :parent
+ def initialize(tag, parent)
+ @tag, @parent = tag, parent
+ super("tag <#{tag}> is missing in tag <#{parent}>")
+ end
+ end
+
+ class TooMuchTagError < InvalidRSSError
+ attr_reader :tag, :parent
+ def initialize(tag, parent)
+ @tag, @parent = tag, parent
+ super("tag <#{tag}> is too much in tag <#{parent}>")
+ end
+ end
+
+ class MissingAttributeError < InvalidRSSError
+ attr_reader :tag, :attribute
+ def initialize(tag, attribute)
+ @tag, @attribute = tag, attribute
+ super("attribute <#{attribute}> is missing in tag <#{tag}>")
+ end
+ end
+
+ class UnknownTagError < InvalidRSSError
+ attr_reader :tag, :uri
+ def initialize(tag, uri)
+ @tag, @uri = tag, uri
+ super("tag <#{tag}> is unknown in namespace specified by uri <#{uri}>")
+ end
+ end
+
+ class NotExceptedTagError < InvalidRSSError
+ attr_reader :tag, :parent
+ def initialize(tag, parent)
+ @tag, @parent = tag, parent
+ super("tag <#{tag}> is not expected in tag <#{parent}>")
+ end
+ end
+
+ class NotAvailableValueError < InvalidRSSError
+ attr_reader :tag, :value
+ def initialize(tag, value)
+ @tag, @value = tag, value
+ super("value <#{value}> of tag <#{tag}> is not available.")
+ end
+ end
+
+ class UnknownConversionMethodError < Error
+ attr_reader :to, :from
+ def initialize(to, from)
+ @to = from
+ @from = from
+ super("can't convert to #{to} from #{from}.")
+ end
+ end
+ # for backward compatibility
+ UnknownConvertMethod = UnknownConversionMethodError
+
+ class ConversionError < Error
+ attr_reader :string, :to, :from
+ def initialize(string, to, from)
+ @string = string
+ @to = from
+ @from = from
+ super("can't convert #{@string} to #{to} from #{from}.")
+ end
+ end
+
+ module BaseModel
+
+ include Utils
+
+ def install_have_child_element(name)
+ attr_accessor name
+ install_element(name) do |n, elem_name|
+ <<-EOC
+ if @#{n}
+ "\#{indent}\#{@#{n}.to_s(convert)}"
+ else
+ ''
+ end
+EOC
+ end
+ end
+ alias_method(:install_have_attribute_element, :install_have_child_element)
+
+ def install_have_children_element(name, postfix="s")
+ def_children_accessor(name, postfix)
+ add_have_children_element(name)
+ install_element(name, postfix) do |n, elem_name|
+ <<-EOC
+ rv = ''
+ @#{n}.each do |x|
+ rv << "\#{indent}\#{x.to_s(convert)}"
+ end
+ rv
+EOC
+ end
+ end
+
+ def install_text_element(name)
+ self::ELEMENTS << name
+ attr_writer name
+ convert_attr_reader name
+ install_element(name) do |n, elem_name|
+ <<-EOC
+ if @#{n}
+ rv = "\#{indent}<#{elem_name}>"
+ value = html_escape(@#{n})
+ if convert and @converter
+ rv << @converter.convert(value)
+ else
+ rv << value
+ end
+ rv << "</#{elem_name}>"
+ rv
+ else
+ ''
+ end
+EOC
+ end
+ end
+
+ def install_date_element(name, type, disp_name=name)
+ self::ELEMENTS << name
+
+ # accessor
+ convert_attr_reader name
+ module_eval(<<-EOC, *get_file_and_line_from_caller(2))
+ def #{name}=(new_value)
+ if new_value.kind_of?(Time)
+ @#{name} = new_value
+ else
+ if @do_validate
+ begin
+ @#{name} = Time.send('#{type}', new_value)
+ rescue ArgumentError
+ raise NotAvailableValueError.new('#{disp_name}', new_value)
+ end
+ elsif /\\A\\s*\\z/ !~ new_value.to_s
+ @#{name} = Time.parse(new_value)
+ else
+ @#{name} = nil
+ end
+ end
+
+ # Is it need?
+ if @#{name}
+ class << @#{name}
+ alias_method(:_to_s, :to_s) unless respond_to?(:_to_s)
+ alias_method(:to_s, :#{type})
+ end
+ end
+
+ end
+EOC
+
+ install_element(name) do |n, elem_name|
+ <<-EOC
+ if @#{n}
+ rv = "\#{indent}<#{elem_name}>"
+ value = html_escape(@#{n}.#{type})
+ if convert and @converter
+ rv << @converter.convert(value)
+ else
+ rv << value
+ end
+ rv << "</#{elem_name}>"
+ rv
+ else
+ ''
+ end
+EOC
+ end
+
+ end
+
+ private
+ def install_element(name, postfix="")
+ elem_name = name.sub('_', ':')
+ module_eval(<<-EOC, *get_file_and_line_from_caller(2))
+ def #{name}_element#{postfix}(convert=true, indent='')
+ #{yield(name, elem_name)}
+ end
+ private :#{name}_element#{postfix}
+EOC
+ end
+
+ def 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}
+ if @converter
+ @converter.convert(@#{attr})
+ else
+ @#{attr}
+ end
+ end
+EOC
+ end
+ end
+
+ def def_children_accessor(accessor_name, postfix="s")
+ module_eval(<<-EOC, *get_file_and_line_from_caller(2))
+ def #{accessor_name}#{postfix}
+ @#{accessor_name}
+ end
+
+ def #{accessor_name}(*args)
+ if args.empty?
+ @#{accessor_name}.first
+ else
+ @#{accessor_name}.send("[]", *args)
+ end
+ end
+
+ def #{accessor_name}=(*args)
+ if args.size == 1
+ @#{accessor_name}.push(args[0])
+ else
+ @#{accessor_name}.send("[]=", *args)
+ end
+ end
+ alias_method(:set_#{accessor_name}, :#{accessor_name}=)
+EOC
+ end
+
+ end
+
+ URI = "http://purl.org/rss/1.0/"
+
+ class Element
+
+ extend BaseModel
+ include Utils
+
+ class << self
+
+ def inherited(klass)
+ klass.module_eval(<<-EOC)
+ public
+
+ TAG_NAME = name.split('::').last.downcase
+
+
+ @@must_call_validators = {::RSS::URI => ''}
+
+ def self.must_call_validators
+ @@must_call_validators
+ end
+
+ def self.install_must_call_validator(prefix, uri)
+ @@must_call_validators[uri] = prefix
+ end
+
+ @@model = []
+
+ def self.model
+ @@model
+ end
+
+ def self.install_model(tag, occurs=nil)
+ if m = @@model.find {|t, o| t == tag}
+ m[1] = occurs
+ else
+ @@model << [tag, occurs]
+ end
+ end
+
+ @@get_attributes = []
+
+ def self.get_attributes()
+ @@get_attributes
+ end
+
+ def self.install_get_attribute(name, uri, required=true)
+ attr_writer name
+ convert_attr_reader name
+ @@get_attributes << [name, uri, required]
+ end
+
+ @@have_content = false
+
+ def self.content_setup
+ attr_writer :content
+ convert_attr_reader :content
+ @@have_content = true
+ end
+
+ def self.have_content?
+ @@have_content
+ end
+
+ @@have_children_elements = []
+
+ def self.have_children_elements
+ @@have_children_elements
+ end
+
+ def self.add_have_children_element(variable_name)
+ @@have_children_elements << variable_name
+ end
+
+ EOC
+ end
+
+ def required_prefix
+ nil
+ end
+
+ def required_uri
+ nil
+ end
+
+ def install_ns(prefix, uri)
+ if self::NSPOOL.has_key?(prefix)
+ raise OverlappedPrefixError.new(prefix)
+ end
+ self::NSPOOL[prefix] = uri
+ end
+
+ end
+
+ attr_accessor :do_validate
+
+ def initialize(do_validate=true)
+ @do_validate = do_validate
+ initialize_have_children_elements
+ end
+
+ def tag_name
+ self.class::TAG_NAME
+ end
+
+ def converter=(converter)
+ @converter = converter
+ children.each do |child|
+ child.converter = converter unless child.nil?
+ end
+ end
+
+ def validate
+ validate_attribute
+ __validate
+ end
+
+ def validate_for_stream(tags)
+ __validate(tags, false)
+ end
+
+ private
+ def initialize_have_children_elements
+ self.class.have_children_elements.each do |variable_name|
+ instance_eval("@#{variable_name} = []")
+ end
+ end
+
+ # not String class children.
+ def children
+ []
+ end
+
+ # default #validate() argument.
+ def _tags
+ []
+ end
+
+ def _attrs
+ []
+ end
+
+ def __validate(tags=_tags, recursive=true)
+ if recursive
+ children.compact.each do |child|
+ child.validate
+ end
+ end
+ must_call_validators = self.class::must_call_validators
+ tags = tag_filter(tags.dup)
+ p tags if $DEBUG
+ self.class::NSPOOL.each do |prefix, uri|
+ if tags.has_key?(uri) and !must_call_validators.has_key?(uri)
+ meth = "#{prefix}_validate"
+ send(meth, tags[uri]) if respond_to?(meth, true)
+ end
+ end
+ must_call_validators.each do |uri, prefix|
+ send("#{prefix}_validate", tags[uri])
+ end
+ end
+
+ def validate_attribute
+ _attrs.each do |a_name, required|
+ if required and send(a_name).nil?
+ raise MissingAttributeError.new(self.class::TAG_NAME, a_name)
+ end
+ end
+ end
+
+ def other_element(convert, indent='')
+ rv = ''
+ private_methods.each do |meth|
+ if /\A([^_]+)_[^_]+_elements?\z/ =~ meth and
+ self.class::NSPOOL.has_key?($1)
+ res = send(meth, convert)
+ rv << "#{indent}#{res}\n" if /\A\s*\z/ !~ res
+ end
+ end
+ rv
+ end
+
+ def _validate(tags, model=self.class.model)
+ count = 1
+ do_redo = false
+ not_shift = false
+ tag = nil
+
+ model.each_with_index do |elem, i|
+
+ if $DEBUG
+ p "before"
+ p tags
+ p elem
+ end
+
+ if not_shift
+ not_shift = false
+ else
+ begin
+ tag = tags.shift
+ rescue NameError
+ end
+ end
+
+ if $DEBUG
+ p "mid"
+ p count
+ end
+
+ case elem[1]
+ when '?'
+ if count > 2
+ raise TooMuchTagError.new(elem[0], tag_name)
+ else
+ if elem[0] == tag
+ do_redo = true
+ else
+ not_shift = true
+ end
+ end
+ when '*'
+ if elem[0] == tag
+ do_redo = true
+ else
+ not_shift = true
+ end
+ when '+'
+ if elem[0] == tag
+ do_redo = true
+ else
+ if count > 1
+ not_shift = true
+ else
+ raise MissingTagError.new(elem[0], tag_name)
+ end
+ end
+ else
+ if elem[0] == tag
+ begin
+ if model[i+1][0] != elem[0] and tags.first == elem[0]
+ raise TooMuchTagError.new(elem[0], tag_name)
+ end
+ rescue NameError # for model[i+1][0] and tags.first
+ end
+ else
+ raise MissingTagError.new(elem[0], tag_name)
+ end
+ end
+
+ if $DEBUG
+ p "after"
+ p not_shift
+ p do_redo
+ p tag
+ end
+
+ if do_redo
+ do_redo = false
+ count += 1
+ redo
+ else
+ count = 1
+ end
+
+ end
+
+ if !tags.nil? and !tags.empty?
+ raise NotExceptedTagError.new(tag, tag_name)
+ end
+
+ end
+
+ def tag_filter(tags)
+ rv = {}
+ tags.each do |tag|
+ rv[tag[0]] = [] unless rv.has_key?(tag[0])
+ rv[tag[0]].push(tag[1])
+ end
+ rv
+ end
+
+ end
+
+end
diff --git a/lib/rss/syndication.rb b/lib/rss/syndication.rb
new file mode 100644
index 0000000000..74bfbac8e1
--- /dev/null
+++ b/lib/rss/syndication.rb
@@ -0,0 +1,81 @@
+require "rss/1.0"
+
+module RSS
+
+ SY_PREFIX = 'sy'
+ SY_URI = "http://purl.org/rss/1.0/modules/syndication/"
+
+ RDF.install_ns(SY_PREFIX, SY_URI)
+
+ module SyndicationModel
+
+ extend BaseModel
+
+ ELEMENTS = []
+
+ %w(updatePeriod updateFrequency).each do |x|
+ install_text_element("#{SY_PREFIX}_#{x}")
+ end
+
+ %w(updateBase).each do |x|
+ install_date_element("#{SY_PREFIX}_#{x}", 'iso8601', x)
+ end
+
+ def sy_validate(tags)
+ counter = {}
+ ELEMENTS.each do |x|
+ counter[x] = 0
+ end
+
+ tags.each do |tag|
+ key = "#{SY_PREFIX}_#{tag}"
+ raise UnknownTagError.new(tag, SY_URI) unless counter.has_key?(key)
+ counter[key] += 1
+ raise TooMuchTagError.new(tag, tag_name) if counter[key] > 1
+ end
+ end
+
+
+ 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
+
+ alias_method(:_sy_updateFrequency=, :sy_updateFrequency=)
+ def sy_updateFrequency=(new_value)
+ new_value = new_value.strip
+ validate_sy_updateFrequency(new_value) if @do_validate
+ self._sy_updateFrequency = new_value.to_i
+ end
+
+ private
+ SY_UPDATEPERIOD_AVAILABLE_VALUES = %w(hourly daily weekly monthly yearly)
+ def validate_sy_updatePeriod(value)
+ unless SY_UPDATEPERIOD_AVAILABLE_VALUES.include?(value)
+ raise NotAvailableValueError.new("updatePeriod", value)
+ end
+ end
+
+ SY_UPDATEFREQUENCY_AVAILABLE_RE = /\A\s*\+?\d+\s*\z/
+ def validate_sy_updateFrequency(value)
+ if SY_UPDATEFREQUENCY_AVAILABLE_RE !~ value
+ raise NotAvailableValueError.new("updateFrequency", value)
+ end
+ end
+
+ end
+
+ class RDF
+ class Channel; include SyndicationModel; end
+ end
+
+ if const_defined? :BaseListener
+ prefix_size = SY_PREFIX.size + 1
+ SyndicationModel::ELEMENTS.each do |x|
+ BaseListener.install_get_text_element(x[prefix_size..-1], SY_URI, "#{x}=")
+ end
+ end
+
+end
diff --git a/lib/rss/taxonomy.rb b/lib/rss/taxonomy.rb
new file mode 100644
index 0000000000..5d3dd5bf85
--- /dev/null
+++ b/lib/rss/taxonomy.rb
@@ -0,0 +1,32 @@
+# Experimental
+
+require "rss/1.0"
+
+module RSS
+
+ TAXO_PREFIX = "taxo"
+ TAXO_NS = "http://purl.org/rss/1.0/modules/taxonomy/"
+
+ Element.install_ns(TAXO_PREFIX, TAXO_NS)
+
+ TAXO_ELEMENTS = []
+
+ %w(link).each do |x|
+ if const_defined? :Listener
+ Listener.install_get_text_element(x, TAXO_NS, "#{TAXO_PREFIX}_#{x}=")
+ end
+ TAXO_ELEMENTS << "#{TAXO_PREFIX}_#{x}"
+ end
+
+ module TaxonomyModel
+ attr_writer *%w(title description creator subject publisher
+ contributor date format identifier source
+ language relation coverage rights).collect{|x| "#{TAXO_PREFIX}_#{x}"}
+ end
+
+ class Channel; extend TaxonomyModel; end
+ class Item; extend TaxonomyModel; end
+ class Image; extend TaxonomyModel; end
+ class TextInput; extend TaxonomyModel; end
+
+end
diff --git a/lib/rss/trackback.rb b/lib/rss/trackback.rb
new file mode 100644
index 0000000000..5c7ca777bd
--- /dev/null
+++ b/lib/rss/trackback.rb
@@ -0,0 +1,235 @@
+# ATTENSION:
+# TrackBack handling API MUST be CHANGED!!!!
+
+require 'rss/1.0'
+require 'rss/2.0'
+
+module RSS
+
+ TRACKBACK_PREFIX = 'trackback'
+ TRACKBACK_URI = 'http://madskills.com/public/xml/rss/module/trackback/'
+
+ RDF.install_ns(TRACKBACK_PREFIX, TRACKBACK_URI)
+ Rss.install_ns(TRACKBACK_PREFIX, TRACKBACK_URI)
+
+ module BaseTrackBackModel
+ def trackback_validate(tags)
+ raise unless @do_validate
+ counter = {}
+ %w(ping about).each do |x|
+ counter["#{TRACKBACK_PREFIX}_#{x}"] = 0
+ end
+
+ tags.each do |tag|
+ key = "#{TRACKBACK_PREFIX}_#{tag}"
+ raise UnknownTagError.new(tag, TRACKBACK_URI) unless counter.has_key?(key)
+ counter[key] += 1
+ if tag != "about" and counter[key] > 1
+ raise TooMuchTagError.new(tag, tag_name)
+ end
+ end
+
+ if counter["#{TRACKBACK_PREFIX}_ping"].zero? and
+ counter["#{TRACKBACK_PREFIX}_about"].nonzero?
+ raise MissingTagError.new("#{TRACKBACK_PREFIX}:ping", tag_name)
+ end
+ end
+ end
+
+ module TrackBackModel10
+ extend BaseModel
+ include BaseTrackBackModel
+
+ def self.append_features(klass)
+ super
+
+ unless klass.class == Module
+ %w(ping).each do |x|
+ klass.install_have_child_element("#{TRACKBACK_PREFIX}_#{x}")
+ end
+
+ %w(about).each do |x|
+ klass.install_have_children_element("#{TRACKBACK_PREFIX}_#{x}")
+ end
+ end
+ end
+
+ class Ping < Element
+ include RSS10
+
+ class << self
+
+ def required_prefix
+ TRACKBACK_PREFIX
+ end
+
+ def required_uri
+ TRACKBACK_URI
+ end
+
+ end
+
+ [
+ ["resource", ::RSS::RDF::URI, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ def initialize(resource=nil)
+ super()
+ @resource = resource
+ end
+
+ def to_s(convert=true)
+ if @resource
+ rv = %Q!<#{TRACKBACK_PREFIX}:ping #{::RSS::RDF::PREFIX}:resource="#{h @resource}"/>!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["resource", true],
+ ]
+ end
+
+ end
+
+ class About < Element
+ include RSS10
+
+ class << self
+
+ def required_prefix
+ TRACKBACK_PREFIX
+ end
+
+ def required_uri
+ TRACKBACK_URI
+ end
+
+ end
+
+ [
+ ["resource", ::RSS::RDF::URI, true]
+ ].each do |name, uri, required|
+ install_get_attribute(name, uri, required)
+ end
+
+ def initialize(resource=nil)
+ super()
+ @resource = resource
+ end
+
+ def to_s(convert=true)
+ if @resource
+ rv = %Q!<#{TRACKBACK_PREFIX}:about #{::RSS::RDF::PREFIX}:resource="#{h @resource}"/>!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ private
+ def _attrs
+ [
+ ["resource", true],
+ ]
+ end
+
+ end
+ end
+
+ module TrackBackModel20
+ include BaseTrackBackModel
+ extend BaseModel
+
+ def self.append_features(klass)
+ super
+
+ unless klass.class == Module
+ %w(ping).each do |x|
+ klass.install_have_child_element("#{TRACKBACK_PREFIX}_#{x}")
+ end
+
+ %w(about).each do |x|
+ klass.install_have_children_element("#{TRACKBACK_PREFIX}_#{x}")
+ end
+ end
+ end
+
+ class Ping < Element
+ include RSS09
+
+ content_setup
+
+ class << self
+
+ def required_prefix
+ TRACKBACK_PREFIX
+ end
+
+ def required_uri
+ TRACKBACK_URI
+ end
+
+ end
+
+ def to_s(convert=true)
+ if @content
+ rv = %Q!<#{TRACKBACK_PREFIX}:ping>#{h @content}</#{TRACKBACK_PREFIX}:ping>!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ end
+
+ class About < Element
+ include RSS09
+
+ content_setup
+
+ class << self
+
+ def required_prefix
+ TRACKBACK_PREFIX
+ end
+
+ def required_uri
+ TRACKBACK_URI
+ end
+
+ end
+
+ def to_s(convert=true)
+ if @content
+ rv = %Q!<#{TRACKBACK_PREFIX}:about>#{h @content}</#{TRACKBACK_PREFIX}:about>!
+ rv = @converter.convert(rv) if convert and @converter
+ rv
+ else
+ ''
+ end
+ end
+
+ end
+ end
+
+ class RDF
+ class Item; include TrackBackModel10; end
+ end
+
+ class Rss
+ class Channel
+ class Item; include TrackBackModel20; end
+ end
+ end
+
+end
diff --git a/lib/rss/utils.rb b/lib/rss/utils.rb
new file mode 100644
index 0000000000..32940cf2a8
--- /dev/null
+++ b/lib/rss/utils.rb
@@ -0,0 +1,19 @@
+module RSS
+
+ module Utils
+
+ def get_file_and_line_from_caller(i=0)
+ tmp = caller[i].split(':')
+ line = tmp.pop.to_i
+ file = tmp.join(':')
+ [file, line]
+ end
+
+ def html_escape(s)
+ s.to_s.gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
+ end
+ alias h html_escape
+
+ end
+
+end
diff --git a/lib/rss/xmlparser.rb b/lib/rss/xmlparser.rb
new file mode 100644
index 0000000000..32061805a6
--- /dev/null
+++ b/lib/rss/xmlparser.rb
@@ -0,0 +1,69 @@
+begin
+ require "xml/encoding-ja"
+rescue LoadError
+ require "xmlencoding-ja"
+ if defined?(Kconv)
+ module XMLEncoding_ja
+ class SJISHandler
+ include Kconv
+ end
+ end
+ end
+end
+
+module RSS
+
+ class XMLParser < ::XML::Parser
+
+ include XML::Encoding_ja
+
+ def listener=(listener)
+ @listener = listener
+ end
+
+ def startElement(name, attrs)
+ @listener.tag_start(name, attrs)
+ end
+
+ def endElement(name)
+ @listener.tag_end(name)
+ end
+
+ def character(data)
+ @listener.text(data)
+ end
+
+ def xmlDecl(version, encoding, standalone)
+ @listener.xmldecl(version, encoding, standalone == 1)
+ end
+
+ end
+
+ class Parser < BaseParser
+
+ private
+ def _parse
+ begin
+ parser = XMLParser.new
+ parser.listener = @listener
+ parser.parse(@rss)
+ rescue XMLParserError => e
+ raise NotWellFormedError.new(parser.line){e.message}
+ end
+ end
+
+ end
+
+ class Listener < BaseListener
+
+ include ListenerMixin
+
+ def xmldecl(version, encoding, standalone)
+ super
+ # Encoding is converted to UTF-8 when XMLParser parses XML.
+ @encoding = 'UTF-8'
+ end
+
+ end
+
+end
diff --git a/lib/rss/xmlscanner.rb b/lib/rss/xmlscanner.rb
new file mode 100644
index 0000000000..81dae96df5
--- /dev/null
+++ b/lib/rss/xmlscanner.rb
@@ -0,0 +1,97 @@
+require 'xmlscan/scanner'
+
+module RSS
+
+ class Parser < BaseParser
+
+ private
+ def _parse
+ begin
+ XMLScan::XMLScanner.new(@listener).parse(@rss)
+ rescue XMLScan::Error => e
+ raise NotWellFormedError.new(e.lineno){e.message}
+ end
+ end
+
+ end
+
+ class Listener < BaseListener
+
+ include XMLScan::Visitor
+ include ListenerMixin
+
+ ENTITIES = {
+ 'lt' => '<',
+ 'gt' => '>',
+ 'amp' => '&',
+ 'quot' => '"',
+ 'apos' => '\''
+ }
+
+ def on_xmldecl_version(str)
+ @version = str
+ end
+
+ def on_xmldecl_encoding(str)
+ @encoding = str
+ end
+
+ def on_xmldecl_standalone(str)
+ @standalone = str
+ end
+
+ def on_xmldecl_end
+ xmldecl(@version, @encoding, @standalone)
+ end
+
+ alias_method(:on_chardata, :text)
+ alias_method(:on_cdata, :text)
+
+ def on_etag(name)
+ tag_end(name)
+ end
+
+ def on_entityref(ref)
+ text(ENTITIES[ref])
+ end
+
+ def on_charref(code)
+ text([code].pack('U'))
+ end
+
+ alias_method(:on_charref_hex, :on_charref)
+
+ def on_stag(name)
+ @attrs = {}
+ end
+
+ def on_attribute(name)
+ @attrs[name] = @current_attr = ''
+ end
+
+ def on_attr_value(str)
+ @current_attr << str
+ end
+
+ def on_attr_entityref(ref)
+ @current_attr << ENTITIES[ref]
+ end
+
+ def on_attr_charref(code)
+ @current_attr << [code].pack('U')
+ end
+
+ alias_method(:on_attr_charref_hex, :on_attr_charref)
+
+ def on_stag_end(name)
+ tag_start(name, @attrs)
+ end
+
+ def on_stag_end_empty(name)
+ tag_start(name, @attrs)
+ tag_end(name)
+ end
+
+ end
+
+end