ruby/lib/rss/rss.rb

606 строки
12 KiB
Ruby
Исходник Обычный вид История

require "time"
require "English"
require "rss/utils"
require "rss/converter"
require "rss/xml-stylesheet"
module RSS
VERSION = "0.0.8"
DEBUG = false
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 = to
@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 = to
@from = from
super("can't convert #{@string} to #{to} from #{from}.")
end
end
module BaseModel
include Utils
def install_have_child_element(name)
add_need_initialize_variable(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")
add_have_children_element(name)
def_children_accessor(name, postfix)
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
add_need_initialize_variable(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
add_need_initialize_variable(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
@@need_initialize_variables = []
def self.add_need_initialize_variable(variable_name)
@@need_initialize_variables << variable_name
end
def self.need_initialize_variables
@@need_initialize_variables
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)
@converter = nil
@do_validate = do_validate
initialize_variables
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_variables
self.class.need_initialize_variables.each do |variable_name|
instance_eval("@#{variable_name} = nil")
end
initialize_have_children_elements
@content = "" if self.class.have_content?
end
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
module RootElementMixin
attr_reader :output_encoding
def initialize(rss_version, version=nil, encoding=nil, standalone=nil)
super()
@rss_version = rss_version
@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
private
def xmldecl
rv = %Q[<?xml version="#{@version}"]
if @output_encoding or @encoding
rv << %Q[ encoding="#{@output_encoding or @encoding}"]
end
rv << %Q[ 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
end
end