1require 'forwardable'
2
3require 'rss/rss'
4
5module RSS
6  module Maker
7    class Base
8      extend Utils::InheritedReader
9
10      OTHER_ELEMENTS = []
11      NEED_INITIALIZE_VARIABLES = []
12
13      class << self
14        def other_elements
15          inherited_array_reader("OTHER_ELEMENTS")
16        end
17        def need_initialize_variables
18          inherited_array_reader("NEED_INITIALIZE_VARIABLES")
19        end
20
21        def inherited_base
22          ::RSS::Maker::Base
23        end
24
25        def inherited(subclass)
26          subclass.const_set(:OTHER_ELEMENTS, [])
27          subclass.const_set(:NEED_INITIALIZE_VARIABLES, [])
28        end
29
30        def add_other_element(variable_name)
31          self::OTHER_ELEMENTS << variable_name
32        end
33
34        def add_need_initialize_variable(variable_name, init_value=nil,
35                                         &init_block)
36          init_value ||= init_block
37          self::NEED_INITIALIZE_VARIABLES << [variable_name, init_value]
38        end
39
40        def def_array_element(name, plural=nil, klass_name=nil)
41          include Enumerable
42          extend Forwardable
43
44          plural ||= "#{name}s"
45          klass_name ||= Utils.to_class_name(name)
46          def_delegators("@#{plural}", :<<, :[], :[]=, :first, :last)
47          def_delegators("@#{plural}", :push, :pop, :shift, :unshift)
48          def_delegators("@#{plural}", :each, :size, :empty?, :clear)
49
50          add_need_initialize_variable(plural) {[]}
51
52          module_eval(<<-EOC, __FILE__, __LINE__ + 1)
53            def new_#{name}
54              #{name} = self.class::#{klass_name}.new(@maker)
55              @#{plural} << #{name}
56              if block_given?
57                yield #{name}
58              else
59                #{name}
60              end
61            end
62            alias new_child new_#{name}
63
64            def to_feed(*args)
65              @#{plural}.each do |#{name}|
66                #{name}.to_feed(*args)
67              end
68            end
69
70            def replace(elements)
71              @#{plural}.replace(elements.to_a)
72            end
73          EOC
74        end
75
76        def def_classed_element_without_accessor(name, class_name=nil)
77          class_name ||= Utils.to_class_name(name)
78          add_other_element(name)
79          add_need_initialize_variable(name) do |object|
80            object.send("make_#{name}")
81          end
82          module_eval(<<-EOC, __FILE__, __LINE__ + 1)
83            private
84            def setup_#{name}(feed, current)
85              @#{name}.to_feed(feed, current)
86            end
87
88            def make_#{name}
89              self.class::#{class_name}.new(@maker)
90            end
91          EOC
92        end
93
94        def def_classed_element(name, class_name=nil, attribute_name=nil)
95          def_classed_element_without_accessor(name, class_name)
96          if attribute_name
97            module_eval(<<-EOC, __FILE__, __LINE__ + 1)
98              def #{name}
99                if block_given?
100                  yield(@#{name})
101                else
102                  @#{name}.#{attribute_name}
103                end
104              end
105
106              def #{name}=(new_value)
107                @#{name}.#{attribute_name} = new_value
108              end
109            EOC
110          else
111            attr_reader name
112          end
113        end
114
115        def def_classed_elements(name, attribute, plural_class_name=nil,
116                                 plural_name=nil, new_name=nil)
117          plural_name ||= "#{name}s"
118          new_name ||= name
119          def_classed_element(plural_name, plural_class_name)
120          local_variable_name = "_#{name}"
121          new_value_variable_name = "new_value"
122          additional_setup_code = nil
123          if block_given?
124            additional_setup_code = yield(local_variable_name,
125                                          new_value_variable_name)
126          end
127          module_eval(<<-EOC, __FILE__, __LINE__ + 1)
128            def #{name}
129              #{local_variable_name} = #{plural_name}.first
130              #{local_variable_name} ? #{local_variable_name}.#{attribute} : nil
131            end
132
133            def #{name}=(#{new_value_variable_name})
134              #{local_variable_name} =
135                #{plural_name}.first || #{plural_name}.new_#{new_name}
136              #{additional_setup_code}
137              #{local_variable_name}.#{attribute} = #{new_value_variable_name}
138            end
139          EOC
140        end
141
142        def def_other_element(name)
143          attr_accessor name
144          def_other_element_without_accessor(name)
145        end
146
147        def def_other_element_without_accessor(name)
148          add_need_initialize_variable(name)
149          add_other_element(name)
150          module_eval(<<-EOC, __FILE__, __LINE__ + 1)
151            def setup_#{name}(feed, current)
152              if !@#{name}.nil? and current.respond_to?(:#{name}=)
153                current.#{name} = @#{name}
154              end
155            end
156          EOC
157        end
158
159        def def_csv_element(name, type=nil)
160          def_other_element_without_accessor(name)
161          attr_reader(name)
162          converter = ""
163          if type == :integer
164            converter = "{|v| Integer(v)}"
165          end
166          module_eval(<<-EOC, __FILE__, __LINE__ + 1)
167            def #{name}=(value)
168              @#{name} = Utils::CSV.parse(value)#{converter}
169            end
170          EOC
171        end
172      end
173
174      attr_reader :maker
175      def initialize(maker)
176        @maker = maker
177        @default_values_are_set = false
178        initialize_variables
179      end
180
181      def have_required_values?
182        not_set_required_variables.empty?
183      end
184
185      def variable_is_set?
186        variables.any? {|var| not __send__(var).nil?}
187      end
188
189      private
190      def initialize_variables
191        self.class.need_initialize_variables.each do |variable_name, init_value|
192          if init_value.nil?
193            value = nil
194          else
195            if init_value.respond_to?(:call)
196              value = init_value.call(self)
197            elsif init_value.is_a?(String)
198              # just for backward compatibility
199              value = instance_eval(init_value, __FILE__, __LINE__)
200            else
201              value = init_value
202            end
203          end
204          instance_variable_set("@#{variable_name}", value)
205        end
206      end
207
208      def setup_other_elements(feed, current=nil)
209        current ||= current_element(feed)
210        self.class.other_elements.each do |element|
211          __send__("setup_#{element}", feed, current)
212        end
213      end
214
215      def current_element(feed)
216        feed
217      end
218
219      def set_default_values(&block)
220        return yield if @default_values_are_set
221
222        begin
223          @default_values_are_set = true
224          _set_default_values(&block)
225        ensure
226          @default_values_are_set = false
227        end
228      end
229
230      def _set_default_values(&block)
231        yield
232      end
233
234      def setup_values(target)
235        set = false
236        if have_required_values?
237          variables.each do |var|
238            setter = "#{var}="
239            if target.respond_to?(setter)
240              value = __send__(var)
241              unless value.nil?
242                target.__send__(setter, value)
243                set = true
244              end
245            end
246          end
247        end
248        set
249      end
250
251      def set_parent(target, parent)
252        target.parent = parent if target.class.need_parent?
253      end
254
255      def variables
256        self.class.need_initialize_variables.find_all do |name, init|
257          # init == "nil" is just for backward compatibility
258          init.nil? or init == "nil"
259        end.collect do |name, init|
260          name
261        end
262      end
263
264      def not_set_required_variables
265        required_variable_names.find_all do |var|
266          __send__(var).nil?
267        end
268      end
269
270      def required_variables_are_set?
271        required_variable_names.each do |var|
272          return false if __send__(var).nil?
273        end
274        true
275      end
276    end
277
278    module AtomPersonConstructBase
279      def self.append_features(klass)
280        super
281
282        klass.class_eval(<<-EOC, __FILE__, __LINE__ + 1)
283          %w(name uri email).each do |element|
284            attr_accessor element
285            add_need_initialize_variable(element)
286          end
287        EOC
288      end
289    end
290
291    module AtomTextConstructBase
292      module EnsureXMLContent
293        class << self
294          def included(base)
295            super
296            base.class_eval do
297              %w(type content xml_content).each do |element|
298                attr_reader element
299                attr_writer element if element != "xml_content"
300                add_need_initialize_variable(element)
301              end
302
303              alias_method(:xhtml, :xml_content)
304            end
305          end
306        end
307
308        def ensure_xml_content(content)
309          xhtml_uri = ::RSS::Atom::XHTML_URI
310          unless content.is_a?(RSS::XML::Element) and
311              ["div", xhtml_uri] == [content.name, content.uri]
312            children = content
313            children = [children] unless content.is_a?(Array)
314            children = set_xhtml_uri_as_default_uri(children)
315            content = RSS::XML::Element.new("div", nil, xhtml_uri,
316                                            {"xmlns" => xhtml_uri},
317                                            children)
318          end
319          content
320        end
321
322        def xml_content=(content)
323          @xml_content = ensure_xml_content(content)
324        end
325
326        def xhtml=(content)
327          self.xml_content = content
328        end
329
330        private
331        def set_xhtml_uri_as_default_uri(children)
332          children.collect do |child|
333            if child.is_a?(RSS::XML::Element) and
334                child.prefix.nil? and child.uri.nil?
335              RSS::XML::Element.new(child.name, nil, ::RSS::Atom::XHTML_URI,
336                                    child.attributes.dup,
337                                    set_xhtml_uri_as_default_uri(child.children))
338            else
339              child
340            end
341          end
342        end
343      end
344
345      def self.append_features(klass)
346        super
347
348        klass.class_eval do
349          include EnsureXMLContent
350        end
351      end
352    end
353
354    module SetupDefaultDate
355      private
356      def _set_default_values
357        keep = {
358          :date => date,
359          :dc_dates => dc_dates.to_a.dup,
360        }
361        _date = _parse_date_if_needed(date)
362        if _date and !dc_dates.any? {|dc_date| dc_date.value == _date}
363          dc_date = self.class::DublinCoreDates::DublinCoreDate.new(self)
364          dc_date.value = _date.dup
365          dc_dates.unshift(dc_date)
366        end
367        self.date ||= self.dc_date
368        super
369      ensure
370        date = keep[:date]
371        dc_dates.replace(keep[:dc_dates])
372      end
373
374      def _parse_date_if_needed(date_value)
375        date_value = Time.parse(date_value) if date_value.is_a?(String)
376        date_value
377      end
378    end
379
380    module SetupDefaultLanguage
381      private
382      def _set_default_values
383        keep = {
384          :dc_languages => dc_languages.to_a.dup,
385        }
386        _language = language
387        if _language and
388            !dc_languages.any? {|dc_language| dc_language.value == _language}
389          dc_language = self.class::DublinCoreLanguages::DublinCoreLanguage.new(self)
390          dc_language.value = _language.dup
391          dc_languages.unshift(dc_language)
392        end
393        super
394      ensure
395        dc_languages.replace(keep[:dc_languages])
396      end
397    end
398
399    class RSSBase < Base
400      class << self
401        def make(*args, &block)
402          new(*args).make(&block)
403        end
404      end
405
406      %w(xml_stylesheets channel image items textinput).each do |element|
407        attr_reader element
408        add_need_initialize_variable(element) do |object|
409          object.send("make_#{element}")
410        end
411        module_eval(<<-EOC, __FILE__, __LINE__ + 1)
412          private
413          def setup_#{element}(feed)
414            @#{element}.to_feed(feed)
415          end
416
417          def make_#{element}
418            self.class::#{Utils.to_class_name(element)}.new(self)
419          end
420        EOC
421      end
422
423      attr_reader :feed_version
424      alias_method(:rss_version, :feed_version)
425      attr_accessor :version, :encoding, :standalone
426
427      def initialize(feed_version)
428        super(self)
429        @feed_type = nil
430        @feed_subtype = nil
431        @feed_version = feed_version
432        @version = "1.0"
433        @encoding = "UTF-8"
434        @standalone = nil
435      end
436
437      def make
438        yield(self)
439        to_feed
440      end
441
442      def to_feed
443        feed = make_feed
444        setup_xml_stylesheets(feed)
445        setup_elements(feed)
446        setup_other_elements(feed)
447        feed.validate
448        feed
449      end
450
451      private
452      remove_method :make_xml_stylesheets
453      def make_xml_stylesheets
454        XMLStyleSheets.new(self)
455      end
456    end
457
458    class XMLStyleSheets < Base
459      def_array_element("xml_stylesheet", nil, "XMLStyleSheet")
460
461      class XMLStyleSheet < Base
462
463        ::RSS::XMLStyleSheet::ATTRIBUTES.each do |attribute|
464          attr_accessor attribute
465          add_need_initialize_variable(attribute)
466        end
467
468        def to_feed(feed)
469          xss = ::RSS::XMLStyleSheet.new
470          guess_type_if_need(xss)
471          set = setup_values(xss)
472          if set
473            feed.xml_stylesheets << xss
474          end
475        end
476
477        private
478        def guess_type_if_need(xss)
479          if @type.nil?
480            xss.href = @href
481            @type = xss.type
482          end
483        end
484
485        def required_variable_names
486          %w(href type)
487        end
488      end
489    end
490
491    class ChannelBase < Base
492      include SetupDefaultDate
493
494      %w(cloud categories skipDays skipHours).each do |name|
495        def_classed_element(name)
496      end
497
498      %w(generator copyright description title).each do |name|
499        def_classed_element(name, nil, "content")
500      end
501
502      [
503       ["link", "href", Proc.new {|target,| "#{target}.href = 'self'"}],
504       ["author", "name"],
505       ["contributor", "name"],
506      ].each do |name, attribute, additional_setup_maker|
507        def_classed_elements(name, attribute, &additional_setup_maker)
508      end
509
510      %w(id about language
511         managingEditor webMaster rating docs ttl).each do |element|
512        attr_accessor element
513        add_need_initialize_variable(element)
514      end
515
516      %w(date lastBuildDate).each do |date_element|
517        attr_reader date_element
518        add_need_initialize_variable(date_element)
519      end
520
521      def date=(_date)
522        @date = _parse_date_if_needed(_date)
523      end
524
525      def lastBuildDate=(_date)
526        @lastBuildDate = _parse_date_if_needed(_date)
527      end
528
529      def pubDate
530        date
531      end
532
533      def pubDate=(date)
534        self.date = date
535      end
536
537      def updated
538        date
539      end
540
541      def updated=(date)
542        self.date = date
543      end
544
545      alias_method(:rights, :copyright)
546      alias_method(:rights=, :copyright=)
547
548      alias_method(:subtitle, :description)
549      alias_method(:subtitle=, :description=)
550
551      def icon
552        image_favicon.about
553      end
554
555      def icon=(url)
556        image_favicon.about = url
557      end
558
559      def logo
560        maker.image.url
561      end
562
563      def logo=(url)
564        maker.image.url = url
565      end
566
567      class SkipDaysBase < Base
568        def_array_element("day")
569
570        class DayBase < Base
571          %w(content).each do |element|
572            attr_accessor element
573            add_need_initialize_variable(element)
574          end
575        end
576      end
577
578      class SkipHoursBase < Base
579        def_array_element("hour")
580
581        class HourBase < Base
582          %w(content).each do |element|
583            attr_accessor element
584            add_need_initialize_variable(element)
585          end
586        end
587      end
588
589      class CloudBase < Base
590        %w(domain port path registerProcedure protocol).each do |element|
591          attr_accessor element
592          add_need_initialize_variable(element)
593        end
594      end
595
596      class CategoriesBase < Base
597        def_array_element("category", "categories")
598
599        class CategoryBase < Base
600          %w(domain content label).each do |element|
601            attr_accessor element
602            add_need_initialize_variable(element)
603          end
604
605          alias_method(:term, :domain)
606          alias_method(:term=, :domain=)
607          alias_method(:scheme, :content)
608          alias_method(:scheme=, :content=)
609        end
610      end
611
612      class LinksBase < Base
613        def_array_element("link")
614
615        class LinkBase < Base
616          %w(href rel type hreflang title length).each do |element|
617            attr_accessor element
618            add_need_initialize_variable(element)
619          end
620        end
621      end
622
623      class AuthorsBase < Base
624        def_array_element("author")
625
626        class AuthorBase < Base
627          include AtomPersonConstructBase
628        end
629      end
630
631      class ContributorsBase < Base
632        def_array_element("contributor")
633
634        class ContributorBase < Base
635          include AtomPersonConstructBase
636        end
637      end
638
639      class GeneratorBase < Base
640        %w(uri version content).each do |element|
641          attr_accessor element
642          add_need_initialize_variable(element)
643        end
644      end
645
646      class CopyrightBase < Base
647        include AtomTextConstructBase
648      end
649
650      class DescriptionBase < Base
651        include AtomTextConstructBase
652      end
653
654      class TitleBase < Base
655        include AtomTextConstructBase
656      end
657    end
658
659    class ImageBase < Base
660      %w(title url width height description).each do |element|
661        attr_accessor element
662        add_need_initialize_variable(element)
663      end
664
665      def link
666        @maker.channel.link
667      end
668    end
669
670    class ItemsBase < Base
671      def_array_element("item")
672
673      attr_accessor :do_sort, :max_size
674
675      def initialize(maker)
676        super
677        @do_sort = false
678        @max_size = -1
679      end
680
681      def normalize
682        if @max_size >= 0
683          sort_if_need[0...@max_size]
684        else
685          sort_if_need[0..@max_size]
686        end
687      end
688
689      private
690      def sort_if_need
691        if @do_sort.respond_to?(:call)
692          @items.sort do |x, y|
693            @do_sort.call(x, y)
694          end
695        elsif @do_sort
696          @items.sort do |x, y|
697            y <=> x
698          end
699        else
700          @items
701        end
702      end
703
704      class ItemBase < Base
705        include SetupDefaultDate
706
707        %w(guid enclosure source categories content).each do |name|
708          def_classed_element(name)
709        end
710
711        %w(rights description title).each do |name|
712          def_classed_element(name, nil, "content")
713        end
714
715        [
716         ["author", "name"],
717         ["link", "href", Proc.new {|target,| "#{target}.href = 'alternate'"}],
718         ["contributor", "name"],
719        ].each do |name, attribute|
720          def_classed_elements(name, attribute)
721        end
722
723        %w(comments id published).each do |element|
724          attr_accessor element
725          add_need_initialize_variable(element)
726        end
727
728        %w(date).each do |date_element|
729          attr_reader date_element
730          add_need_initialize_variable(date_element)
731        end
732
733        def date=(_date)
734          @date = _parse_date_if_needed(_date)
735        end
736
737        def pubDate
738          date
739        end
740
741        def pubDate=(date)
742          self.date = date
743        end
744
745        def updated
746          date
747        end
748
749        def updated=(date)
750          self.date = date
751        end
752
753        alias_method(:summary, :description)
754        alias_method(:summary=, :description=)
755
756        def <=>(other)
757          _date = date || dc_date
758          _other_date = other.date || other.dc_date
759          if _date and _other_date
760            _date <=> _other_date
761          elsif _date
762            1
763          elsif _other_date
764            -1
765          else
766            0
767          end
768        end
769
770        class GuidBase < Base
771          %w(isPermaLink content).each do |element|
772            attr_accessor element
773            add_need_initialize_variable(element)
774          end
775
776          def permanent_link?
777            isPermaLink
778          end
779
780          def permanent_link=(bool)
781            self.isPermaLink = bool
782          end
783        end
784
785        class EnclosureBase < Base
786          %w(url length type).each do |element|
787            attr_accessor element
788            add_need_initialize_variable(element)
789          end
790        end
791
792        class SourceBase < Base
793          include SetupDefaultDate
794
795          %w(authors categories contributors generator icon
796             logo rights subtitle title).each do |name|
797            def_classed_element(name)
798          end
799
800          [
801           ["link", "href"],
802          ].each do |name, attribute|
803            def_classed_elements(name, attribute)
804          end
805
806          %w(id content).each do |element|
807            attr_accessor element
808            add_need_initialize_variable(element)
809          end
810
811          alias_method(:url, :link)
812          alias_method(:url=, :link=)
813
814          %w(date).each do |date_element|
815            attr_reader date_element
816            add_need_initialize_variable(date_element)
817          end
818
819          def date=(_date)
820            @date = _parse_date_if_needed(_date)
821          end
822
823          def updated
824            date
825          end
826
827          def updated=(date)
828            self.date = date
829          end
830
831          private
832          AuthorsBase = ChannelBase::AuthorsBase
833          CategoriesBase = ChannelBase::CategoriesBase
834          ContributorsBase = ChannelBase::ContributorsBase
835          GeneratorBase = ChannelBase::GeneratorBase
836
837          class IconBase < Base
838            %w(url).each do |element|
839              attr_accessor element
840              add_need_initialize_variable(element)
841            end
842          end
843
844          LinksBase = ChannelBase::LinksBase
845
846          class LogoBase < Base
847            %w(uri).each do |element|
848              attr_accessor element
849              add_need_initialize_variable(element)
850            end
851          end
852
853          class RightsBase < Base
854            include AtomTextConstructBase
855          end
856
857          class SubtitleBase < Base
858            include AtomTextConstructBase
859          end
860
861          class TitleBase < Base
862            include AtomTextConstructBase
863          end
864        end
865
866        CategoriesBase = ChannelBase::CategoriesBase
867        AuthorsBase = ChannelBase::AuthorsBase
868        LinksBase = ChannelBase::LinksBase
869        ContributorsBase = ChannelBase::ContributorsBase
870
871        class RightsBase < Base
872          include AtomTextConstructBase
873        end
874
875        class DescriptionBase < Base
876          include AtomTextConstructBase
877        end
878
879        class ContentBase < Base
880          include AtomTextConstructBase::EnsureXMLContent
881
882          %w(src).each do |element|
883            attr_accessor(element)
884            add_need_initialize_variable(element)
885          end
886
887          def xml_content=(content)
888            content = ensure_xml_content(content) if inline_xhtml?
889            @xml_content = content
890          end
891
892          alias_method(:xml, :xml_content)
893          alias_method(:xml=, :xml_content=)
894
895          def inline_text?
896            [nil, "text", "html"].include?(@type)
897          end
898
899          def inline_html?
900            @type == "html"
901          end
902
903          def inline_xhtml?
904            @type == "xhtml"
905          end
906
907          def inline_other?
908            !out_of_line? and ![nil, "text", "html", "xhtml"].include?(@type)
909          end
910
911          def inline_other_text?
912            return false if @type.nil? or out_of_line?
913            /\Atext\//i.match(@type) ? true : false
914          end
915
916          def inline_other_xml?
917            return false if @type.nil? or out_of_line?
918            /[\+\/]xml\z/i.match(@type) ? true : false
919          end
920
921          def inline_other_base64?
922            return false if @type.nil? or out_of_line?
923            @type.include?("/") and !inline_other_text? and !inline_other_xml?
924          end
925
926          def out_of_line?
927            not @src.nil? and @content.nil?
928          end
929        end
930
931        class TitleBase < Base
932          include AtomTextConstructBase
933        end
934      end
935    end
936
937    class TextinputBase < Base
938      %w(title description name link).each do |element|
939        attr_accessor element
940        add_need_initialize_variable(element)
941      end
942    end
943  end
944end
945