# 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 << '
' 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}