# makerss.rb # # generate RSS file when updating. # # options configurable through settings: # @conf['makerss.hidecontent'] : hide full-text content. default: false # @conf['makerss.shortdesc'] : shorter description. default: false # @conf['makerss.comment_link'] : insert tsukkomi's link. default: false # # options to be edited in tdiary.conf: # @conf['makerss.file'] : local file name of RSS file. default: 'index.rdf'. # @conf['makerss.url'] : URL of RSS file. # @conf['makerss.no_comments.file'] : local file name of RSS file without # comments. default: 'no_comments.rdf'. # @conf['makerss.no_comments.url'] : URL of RSS file without TSUKOMI. # @conf.banner : URL of site banner image (can be relative) # @conf.description : desciption of the diary # @conf['makerss.partial'] : how much portion of body to be in description # used when makerss.shortdesc, default: 0.25 # @conf['makerss.suffix'] : strings which are appended to the title tag. # @conf['makerss.no_comments.suffix'] : strings which are appended to # the title tag of the commentless rdf. # # CAUTION: Before using, make 'index.rdf' and 'no_comments.rdf' file # into the directory of your diary, and permit writable to httpd. # # Copyright (c) 2009 TADA Tadashi # Distributed under the GPL2 or any later version. # if /^append|replace|comment|showcomment|startup$/ =~ @mode then unless @conf.description @conf.description = @conf['whatsnew_list.rdf.description'] end module ::TDiary class RDFSection attr_reader :id, :time, :section def self.from_json(id, json) self.new(id, nil, nil, data: JSON.load(json)) end # 'id' has 'YYYYMMDDpNN' format (p or c). # 'time' is Last-Modified this section as a Time object. def initialize( id, time = nil, section = nil, opts = {} ) @id = id if opts[:data] @time = opts[:data]['time'] @is_comment = opts[:data]['is_comment'] @section = opts[:data]['section'] else @time = time_string(time) @is_comment = section.respond_to?(:name) @section = section_to_hash(section) end end def body? !@is_comment end def <=>( other ) other.time <=> @time end def to_json { 'id' => @id, 'time' => @time, 'section' => @section, 'is_comment' => @is_comment }.to_json end private def time_string(time) g = time.dup.gmtime l = Time::local( g.year, g.month, g.day, g.hour, g.min, g.sec ) tz = (g.to_i - l.to_i) zone = sprintf( "%+03d:%02d", tz / 3600, tz % 3600 / 60 ) time.strftime( "%Y-%m-%dT%H:%M:%S" ) + zone end def section_to_hash(section) sec ||= {} sec['body'] = section.respond_to?(:body_to_html) ? section.body_to_html : section.body sec['subtitle'] = section.subtitle_to_html if section.respond_to?(:subtitle_to_html) sec['visibility'] = section.visible? rescue true sec['category'] = section.categories rescue [] sec['name'] = section.name if section.respond_to?(:name) return sec end end end end @makerss_rsses = @makerss_rsses || [] class MakeRssFull include ERB::Util include TDiary::ViewHelper def initialize(conf, cgi = CGI.new) @conf, @cgi = conf, cgi @item_num = 0 end def title @conf['makerss.suffix'] || '' end def head( str ) @head = str @head.sub!( /<\/title>/, "#{h title}" ) end def foot( str ); @foot = str; end def image( str ); @image = str; end def banner( str ); @banner = str; end def item( seq, body, rdfsec ) @item_num += 1 return if @item_num > 15 @seq = '' unless @seq @seq << seq @body = '' unless @body @body << body end def xml xml = @head.to_s xml << @image.to_s xml << "\n" xml << @seq.to_s xml << "\n\n" xml << @banner.to_s xml << @body.to_s xml << @foot.to_s xml.gsub( /[\x00-\x1f]/ ){|s| s =~ /[\r\n\t]/ ? s : ""} end def file f = @conf['makerss.file'] || 'index.rdf' f = 'index.rdf' if f.empty? f =~ %r|^/| ? f : "#{document_root}/#{f}" end def writable? if FileTest::writable?( file ) then return true elsif FileTest::exist?( file ) return false else # try to create begin File::open( file, 'w' ){|f|} return true rescue return false end end end def write( encoder ) begin File::open( file, 'w' ) do |f| f.write( encoder.call( xml ) ) end rescue end end def url u = @conf['makerss.url'] || "#{base_url}#{File.basename(file)}" u = "#{base_url}#{File.basename(file)}" if u.empty? u end def document_root if @cgi.is_a?(RackCGI) File.join(TDiary.server_root, 'public') else TDiary.server_root end end end @makerss_rsses << MakeRssFull::new(@conf, @cgi) class MakeRssNoComments < MakeRssFull def title @conf['makerss.no_comments.suffix'] || '(without comments)' end def item( seq, body, rdfsec ) return unless rdfsec.body? super end def file f = @conf['makerss.no_comments.file'] || 'no_comments.rdf' f = 'no_comments.rdf' if f.empty? f =~ %r|^/| ? f : "#{document_root}/#{f}" end def write( encoder ) return unless @conf['makerss.no_comments'] super( encoder ) end def url return nil unless @conf['makerss.no_comments'] u = @conf['makerss.no_comments.url'] || "#{base_url}#{File.basename(file)}" u = "#{base_url}#{File.basename(file)}" if u.empty? u end end @makerss_rsses << MakeRssNoComments::new(@conf, @cgi) def makerss_update def get(db, id) json = db.get(id) return nil unless json RDFSection.from_json(id, json) rescue nil end def set(db, id, section) db.set(id, section.to_json) end date = @date.strftime( "%Y%m%d" ) diary = @diaries[date] uri = @conf.index.dup uri[0, 0] = base_url if %r|^https?://|i !~ @conf.index uri.gsub!( %r|/\./|, '/' ) rsses = @makerss_rsses transaction('makerss') do |db| begin if /^append|replace$/ =~ @mode then format = "#{date}p%02d" index = 0 diary.each_section do |section| index += 1 id = format % index if diary.visible? and !get(db, id) then set(db, id, RDFSection::new( id, Time::now, section )) elsif !diary.visible? and get(db, id) db.delete(id) elsif diary.visible? and get(db, id) if get(db, id).section['body'] != section.body_to_html or get(db, id).section['subtitle'] != section.subtitle_to_html then set(db, id, RDFSection::new( id, Time::now, section )) end end end loop do index += 1 id = format % index if get(db, id) then db.delete(id) else break end end elsif /^comment$/ =~ @mode and @conf.show_comment id = "#{date}c%02d" % diary.count_comments( true ) set(db, id, RDFSection::new( id, @comment.date, @comment )) elsif /^showcomment$/ =~ @mode index = 0 diary.each_comment do |comment| index += 1 id = "#{date}c%02d" % index if !get(db, id) and (@conf.show_comment and comment.visible? and /^(TrackBack|Pingback)$/i !~ comment.name) then set(db, id, RDFSection::new( id, comment.date, comment )) elsif get(db, id) and !(@conf.show_comment and comment.visible? and /^(TrackBack|Pingback)$/i !~ comment.name) db.delete(id) end end end rsses.each{|rss| rss.head( makerss_header( uri ) ) } db.keys.map{|k|get(db, k)}.sort.each_with_index do |rdfsec, idx| if rdfsec && rdfsec.section['visibility'] rsses.each {|rss| rss.item( makerss_seq( uri, rdfsec ), makerss_body( uri, rdfsec ), rdfsec ) } end if idx > 50 db.delete(rdfsec.id) end end end end if @conf.banner and not @conf.banner.empty? if /^http/ =~ @conf.banner rdf_image = @conf.banner else rdf_image = base_url + @conf.banner end rsses.each {|r| r.image( %Q[\n] ) } end rsses.each {|r| r.banner( makerss_banner( uri, rdf_image ) ) if rdf_image r.foot( makerss_footer ) r.write( Proc::new{|s| replace_entities( to_utf8( s ) )} ) } end def makerss_header( uri ) rdf_url = @conf['makerss.url'] || "#{base_url}index.rdf" rdf_url = "#{base_url}index.rdf" if rdf_url.empty? desc = @conf.description || '' copyright = Time::now.strftime( "Copyright %Y #{@conf.author_name}" ) copyright += " <#{@conf.author_mail}>" if @conf.author_mail and not @conf.author_mail.empty? copyright += ", copyright of comments by respective authors" %Q[ #{h @conf.html_title} #{h uri} #{h desc} #{h @conf.author_name} #{h copyright} ] end def makerss_seq( uri, rdfsec ) %Q|\n| end def makerss_banner( uri, rdf_image ) %Q[ #{h @conf.html_title} #{h rdf_image} #{h uri} ] end def makerss_desc_shorten( text ) if @conf['makerss.shortdesc'] then @conf['makerss.partial'] = 0.25 unless @conf['makerss.partial'] len = ( text.size.to_f * @conf['makerss.partial'] ).ceil.to_i len = 500 if len > 500 else len = 500 end @conf.shorten( text, len ) end def feed? @makerss_in_feed end def makerss_body( uri, rdfsec ) rdf = "" if rdfsec.body? then rdf = %Q|\n| rdf << %Q|#{h uri}#{anchor rdfsec.id}\n| rdf << %Q|#{h rdfsec.time}\n| a = rdfsec.id.scan( /(\d{4})(\d\d)(\d\d)/ ).flatten.map{|s| s.to_i} date = Time::local( *a ) old_apply_plugin = @conf['apply_plugin'] @conf['apply_plugin'] = true @makerss_in_feed = true subtitle = rdfsec.section['subtitle'] body_enter = body_enter_proc( date ) body = apply_plugin( rdfsec.section['body'] ) body_leave = body_leave_proc( date ) @makerss_in_feed = false sub = (subtitle || '').sub( /^(\[([^\]]+)\])+ */, '' ) sub = apply_plugin( sub, true ).strip if sub.empty? sub = @conf.shorten( remove_tag( body ).strip, 20 ) end rdf << %Q|#{sub}\n| rdf << %Q|#{h @conf.author_name}\n| rdfsec.section['category'].each do |category| rdf << %Q|#{h category}\n| end desc = remove_tag( body ).strip desc.gsub!( /&.*?;/, '' ) rdf << %Q|#{h makerss_desc_shorten( desc )}\n| unless @conf['makerss.hidecontent'] text = '' text << '

' + apply_plugin( subtitle.sub( /^(\[([^\]]+)\])+ */, '' ) ).strip + '

' if subtitle and not subtitle.empty? text << body_enter text << body text << body_leave unless text.empty? uri = @conf.index.dup uri[0, 0] = base_url unless %r|^https?://|i =~ uri uri.gsub!( %r|/\./|, '/' ) text = absolutify( text, uri ) text.gsub!( /\]\]>/, ']]]]>' ) rdf << %Q|#{comment_new}

| end rdf << %Q|]]>
\n| end end @conf['apply_plugin'] = old_apply_plugin rdf << "
\n" else # TSUKKOMI rdf = %Q|\n| rdf << %Q|#{h uri}#{anchor rdfsec.id}\n| rdf << %Q|#{h rdfsec.time}\n| rdf << %Q|#{makerss_tsukkomi_label( rdfsec.id )} (#{h rdfsec.section['name']})\n| rdf << %Q|#{h rdfsec.section['name']}\n| text = rdfsec.section['body'] rdf << %Q|#{h makerss_desc_shorten( text )}\n| unless @conf['makerss.hidecontent'] rdf << %Q|' ).gsub( /

\Z/, '' ).gsub( /\]\]>/, ']]]]>' )}]]>
\n| end rdf << "
\n" end rdf end def makerss_footer "
\n" end add_update_proc do makerss_update unless @cgi.params['makerss_update'][0] == 'false' end add_header_proc { html = '' @makerss_rsses.each do |rss| next unless rss.url html << %Q|\t\n| end html } add_conf_proc( 'makerss', @makerss_conf_label, 'update' ) do if @mode == 'saveconf' then %w( hidecontent shortdesc comment_link no_comments).each do |s| item = "makerss.#{s}" @conf[item] = ( 't' == @cgi.params[item][0] ) end end @makerss_rsses.each do |rss| if rss.class == MakeRssFull then @makerss_full = rss elsif rss.class == MakeRssNoComments @makerss_no_comments = rss end end makerss_conf_html end add_edit_proc do checked = if @cgi.params['makerss_update'][0] == 'false' then ' checked' elsif @date < (Time::now - 30*24*60*60) # older over a month ' checked' else '' end <<-HTML
HTML end add_startup_proc do makerss_update end def replace_entities( text ) unless @xml_entity_table then @xml_entity_table = { ' ' => ' ', '¡' => '¡', '¢' => '¢', '£' => '£', '¤' => '¤', '¥' => '¥', '¦' => '¦', '§' => '§', '¨' => '¨', '©' => '©', 'ª' => 'ª', '«' => '«', '¬' => '¬', '­' => '­', '®' => '®', '¯' => '¯', '°' => '°', '±' => '±', '²' => '²', '³' => '³', '´' => '´', 'µ' => 'µ', '¶' => '¶', '·' => '·', '¸' => '¸', '¹' => '¹', 'º' => 'º', '»' => '»', '¼' => '¼', '½' => '½', '¾' => '¾', '¿' => '¿', 'À' => 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', '&Aelig;' => 'Æ', 'Ç' => 'Ç', 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', 'Ð' => 'Ð', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', '×' => '×', 'Ø' => 'Ø', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'Þ' => 'Þ', 'ß' => 'ß', 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'æ' => 'æ', 'ç' => 'ç', 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', 'ð' => 'ð', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', '÷' => '÷', 'ø' => 'ø', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'þ' => 'þ', 'ÿ' => 'ÿ', 'Œ' => 'Œ', 'œ' => 'œ', 'Š' => 'Š', 'š' => 'š', 'Ÿ' => 'Ÿ', 'ˆ' => 'ˆ', '˜' => '˜', ' ' => ' ', ' ' => ' ', ' ' => ' ', '‌' => '‌', '‍' => '‍', '‎' => '‎', '‏' => '‏', '–' => '–', '—' => '—', '‘' => '‘', '’' => '’', '‚' => '‚', '“' => '“', '”' => '”', '„' => '„', '†' => '†', '‡' => '‡', '‰' => '‰', '‹' => '‹', '›' => '›', '€' => '€', 'ƒ' => 'ƒ', 'Α' => 'Α', 'Β' => 'Β', 'Γ' => 'Γ', 'Δ' => 'Δ', 'Ε' => 'Ε', 'Ζ' => 'Ζ', 'Η' => 'Η', 'Θ' => 'Θ', 'Ι' => 'Ι', 'Κ' => 'Κ', 'Λ' => 'Λ', 'Μ' => 'Μ', 'Ν' => 'Ν', 'Ξ' => 'Ξ', 'Ο' => 'Ο', 'Π' => 'Π', 'Ρ' => 'Ρ', 'Σ' => 'Σ', 'Τ' => 'Τ', 'Υ' => 'Υ', 'Φ' => 'Φ', 'Χ' => 'Χ', 'Ψ' => 'Ψ', 'Ω' => 'Ω', 'α' => 'α', 'β' => 'β', 'γ' => 'γ', 'δ' => 'δ', 'ε' => 'ε', 'ζ' => 'ζ', 'η' => 'η', 'θ' => 'θ', 'ι' => 'ι', 'κ' => 'κ', 'λ' => 'λ', 'μ' => 'μ', 'ν' => 'ν', 'ξ' => 'ξ', 'ο' => 'ο', 'π' => 'π', 'ρ' => 'ρ', 'ς' => 'ς', 'σ' => 'σ', 'τ' => 'τ', 'υ' => 'υ', 'φ' => 'φ', 'χ' => 'χ', 'ψ' => 'ψ', 'ω' => 'ω', 'ϑ' => 'ϑ', 'ϒ' => 'ϒ', 'ϖ' => 'ϖ', '•' => '•', '…' => '…', '′' => '′', '″' => '″', '‾' => '‾', '⁄' => '⁄', '℘' => '℘', 'ℑ' => 'ℑ', 'ℜ' => 'ℜ', '™' => '™', 'ℵ' => 'ℵ', '←' => '←', '→' => '→', '↓' => '↓', '↔' => '↔', '↵' => '↵', '⇐' => '⇐', '⇑' => '⇑', '⇒' => '⇒', '⇓' => '⇓', '⇔' => '⇔', '∀' => '∀', '∂' => '∂', '∃' => '∃', '∅' => '∅', '∇' => '∇', '∈' => '∈', '∉' => '∉', '∋' => '∋', '∏' => '∏', '∑' => '∑', '−' => '−', '∗' => '∗', '√' => '√', '∝' => '∝', '∞' => '∞', '∠' => '∠', '∧' => '∧', '∨' => '∨', '∩' => '∩', '∪' => '∪', '∫' => '∫', '∴' => '∴', '∼' => '∼', '≅' => '≅', '≈' => '≈', '≠' => '≠', '≡' => '≡', '≤' => '≤', '≥' => '≥', '⊂' => '⊂', '⊃' => '⊃', '⊄' => '⊄', '⊆' => '⊆', '⊇' => '⊇', '⊕' => '⊕', '⊗' => '⊗', '⊥' => '⊥', '⋅' => '⋅', '⌈' => '⌈', '⌉' => '⌉', '⌊' => '⌊', '⌋' => '⌋', '⟨' => '〈', '⟩' => '〉', '◊' => '◊', '♠' => '♠', '♣' => '♣', '♥' => '♥', '♦' => '♦' } end text.gsub( /&[a-z]+;/im ) do |e| @xml_entity_table[e] || e end end # Copied from below which includes some tests # http://github.com/zunda/ruby-absolutify/tree/master def absolutify(html, baseurl) @@_absolutify_attr_regexp ||= Hash.new baseuri = URI.parse(baseurl) r = html.gsub(%r|<\S[^>]*/?>|) do |tag| type = tag.scan(/\A<(\S+)/)[0][0].downcase if attr = {'a' => 'href', 'img' => 'src'}[type] @@_absolutify_attr_regexp[attr] ||= %r|(.*#{attr}\s*=\s*)(['"]?)([^\2>]+?)(\2.*)|im m = tag.match(@@_absolutify_attr_regexp[attr]) unless m.nil? prefix = m[1] + m[2] location = m[3] postfix = m[4] begin uri = URI.parse(location) if uri.relative? location = (baseuri + location).to_s elsif not uri.host and uri.path path = uri.path path += '?' + uri.query if uri.query path += '#' + uri.fragment if uri.fragment location = (baseuri + path).to_s end tag = prefix + location + postfix rescue URI::InvalidURIError end end end tag end return r end # Local Variables: # mode: ruby # indent-tabs-mode: t # tab-width: 3 # ruby-indent-level: 3 # End: