From 8640f16308dce0b013af0470b926956e676ccc1a Mon Sep 17 00:00:00 2001 From: matz Date: Wed, 28 Jan 2004 03:46:13 +0000 Subject: * lib/rss: rss library imported. [ruby-dev:22726] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@5566 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/rss/0.9.rb | 438 +++++++++++++++++++++++++++++++++ lib/rss/1.0.rb | 652 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/rss/2.0.rb | 148 +++++++++++ lib/rss/content.rb | 47 ++++ lib/rss/converter.rb | 182 ++++++++++++++ lib/rss/dublincore.rb | 56 +++++ lib/rss/parser.rb | 330 +++++++++++++++++++++++++ lib/rss/rexmlparser.rb | 43 ++++ lib/rss/rss.rb | 536 ++++++++++++++++++++++++++++++++++++++++ lib/rss/syndication.rb | 81 ++++++ lib/rss/taxonomy.rb | 32 +++ lib/rss/trackback.rb | 235 ++++++++++++++++++ lib/rss/utils.rb | 19 ++ lib/rss/xmlparser.rb | 69 ++++++ lib/rss/xmlscanner.rb | 97 ++++++++ 15 files changed, 2965 insertions(+) create mode 100644 lib/rss/0.9.rb create mode 100644 lib/rss/1.0.rb create mode 100644 lib/rss/2.0.rb create mode 100644 lib/rss/content.rb create mode 100644 lib/rss/converter.rb create mode 100644 lib/rss/dublincore.rb create mode 100644 lib/rss/parser.rb create mode 100644 lib/rss/rexmlparser.rb create mode 100644 lib/rss/rss.rb create mode 100644 lib/rss/syndication.rb create mode 100644 lib/rss/taxonomy.rb create mode 100644 lib/rss/trackback.rb create mode 100644 lib/rss/utils.rb create mode 100644 lib/rss/xmlparser.rb create mode 100644 lib/rss/xmlscanner.rb (limited to 'lib') 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} + +#{channel_element(false)} +#{other_element(false, "\t")} + +EOR + rv = @converter.convert(rv) if convert and @converter + rv + end + + private + def xmldecl + 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 + + #{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")} + +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 + + #{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")} + +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 + +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 + + #{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")} + +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! ! + rv << %Q!#{@content}! + 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!! + 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!! + rv << %Q!#{h @content}! + 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 + + #{title_element(false)} + #{description_element(false)} + #{name_element(false)} + #{link_element(false)} +#{other_element(false, "\t\t\t\t")} + +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")} + +EORDF + rv = @converter.convert(rv) if convert and @converter + rv + end + + private + def xmldecl + 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")} + +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 + + #{title_element(false)} + #{link_element(false)} + #{description_element(false)} + #{image_element(false)} +#{items_element(false)} + #{textinput_element(false)} +#{other_element(false, "\t\t")} + +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!! + 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|| + 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 + +#{Seq_element(convert)} +#{other_element(convert, "\t\t\t")} + +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 + + #{title_element(false)} + #{url_element(false)} + #{link_element(false)} +#{other_element(false, "\t\t")} + +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 + + #{title_element(false)} + #{link_element(false)} + #{description_element(false)} +#{other_element(false, "\t\t")} + +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 + + #{title_element(false)} + #{description_element(false)} + #{name_element(false)} + #{link_element(false)} +#{other_element(false, "\t\t")} + +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!#{h @content}! + 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 << "" + 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 << "" + 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}! + 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}! + 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(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/ 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 -- cgit v1.2.3