<blog_newpost(목차페이지이름)>
* [[/제목]] 날짜
<blog_calendar(목차페이지이름,년,월)>
<blog_includeorder(목차페이지,시작순서,끝순서)>
<blog_includeperiod(목차페이지,시작날짜,끝날짜)>
<blog_listorder(목차페이지,시작순서,끝순서[,날짜출력방식])>
<blog_listperiod(목차페이지,시작날짜,끝날짜[,날짜출력방식])>
<blog_prevnext(목차페이지)>
<blog_prevnext>
<blog_rss(목차페이지[,블로그페이지])>
<blog_rss> <channel> [ 채널에 관한 엘리멘트들 ] [ <item> 아이템에 관한 엘리멘트들 </item> ] </channel> </blog_rss>
<blog_rss> - 필수 <channel> - 필수 <title>블로그제목</title> --+ <link>http://...블로그주소</link> +-- 이 부분은 채널에 관한 정보. (생략 가능) <description>블로그 설명</description> --+ 채널에 관한 정보는 목차페이지에 있을 때만 의미가 있다. <item> --+ <author>저자</author> +-- 이것은 아이템에 관한 정보. (생략 가능) </item> --+ 목차페이지에 있는 경우는, RSS의 각 아이템에 일괄 적용된다. 포스트 페이지에 있는 경우는, 그 페이지에 해당하는 아이템에만 적용된다. </channel> - 필수 </blog_rss> - 필수
wiki.pl?action=blog_rss&listpage=목차페이지[&blogpage=블로그페이지][&items=아이템갯수]
# <blog_newpost(목차페이지)> # 목차페이지에 새 글 제목과 날짜를 입력하는 폼을 출력 # 저장 버튼을 누르면 이 매크로 있는 자리에 치환됨 sub blog_newpost { my ($txt) = @_; $txt =~ s/\&__LT__;blog_newpost\((.+)\)\&__GT__;/&MacroBlogNewPost($1)/gei; return $txt; } sub MacroBlogNewPost { use strict; my ($id) = @_; my $txt; $id = &RemoveLink($id); $id = &FreeToNormal($id); if (my $temp = &ValidId($id)) { return "<font color='red'>$temp</font>"; } my ($readonly_true, $readonly_style, $readonly_msg); my ($title_field, $date_field, $submit_button); if (!&UserCanEdit($id,1)) { $readonly_true = "true"; $readonly_style = "background-color: #f0f0f0;"; $readonly_msg = T('Editing Denied'); $submit_button = ""; $title_field = $q->textfield(-name=>"title", -class=>"comments", -size=>"50", -maxlength=>"100", -readonly=>"$readonly_true", -style=>"$readonly_style", -default=>"$readonly_msg"); $date_field = $q->textfield(-name=>"date", -class=>"comments", -size=>"10", -maxlength=>"10", -readonly=>"$readonly_true", -style=>"$readonly_style", -default=>""); } else { my $today = &CalcDay($Now); $today =~ s/-(\d)-/-0$1-/g; $today =~ s/-(\d)$/-0$1/g; $submit_button = $q->submit(-name=>"Submit",-value=>T("Submit")); $title_field = $q->textfield(-name=>"title", -class=>"comments", -size=>"50", -maxlength=>"100", -default=>""); $date_field = $q->textfield(-name=>"date", -class=>"comments", -size=>"10", -maxlength=>"10", -default=>"$today"); } $txt = $q->startform(-name=>"newpost",-method=>"POST",-action=>"$ScriptName") . &GetHiddenValue("action","blog_newpost") . &GetHiddenValue("id","$id") . &GetHiddenValue("pageid","$pageid") . T('Title will be converted into [[/Title]] automatically.')."<BR>". T('Title:')." ". $title_field." ". T('Date').": ". $date_field." ". $submit_button. $q->endform; return $txt; } 1;
# <blog_calendar(목차페이지,년,월)> sub blog_calendar { my ($txt) = @_; $txt =~ s/\&__LT__;blog_calendar\(([^,\n]+),([-+]?\d+),([-+]?\d+)\)\&__GT__;/&MacroBlogCalendar($1, $2, $3)/gei; return $txt; } sub MacroBlogCalendar { use Time::Local; use strict; my ($tocpage, $cal_year, $cal_month) = @_; my $result=''; my $cal_result=''; my $cal_page; my @cal_color = ("red", "black", "black", "black", "black", "black", "blue", "green"); my @cal_dow = (T('Su'), T('Mo'), T('Tu'), T('We'), T('Th'), T('Fr'), T('Sa')); my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($Now+$TimeZoneOffset); my ($this_year, $this_month, $this_day) = ($year, $mon, $mday); my $cal_time; my ($td_class, $span_style); my $temp; # 달의 값이 13 이상이면 무효 if (!($cal_month =~ /[-+]/) && ($cal_month > 12)) { return "<calendar($tocpage,$cal_year,$cal_month)>"; } # 라이브러리 읽음 my ($MacrosDir, $MyMacrosDir) = ("./macros/", "./mymacros/"); if (-f "$MyMacrosDir/blog_library.pl") { require "./$MyMacrosDir/blog_library.pl"; } elsif (-f "$MacrosDir/blog_library.pl") { require "./$MacrosDir/blog_library.pl"; } else { return "<font color='red'>blog_library.pl not found</font>"; } # 목차페이지로부터 목차리스트를 얻어냄 my ($status, $toc_mainpage, @tocitem_List) = &BlogReadToc($tocpage); if (!$status) { return "$toc_mainpage"; } # 리스트를 하나로 연결 my $tocitems = join("\n", @tocitem_List); # 년도나 달에 0 을 인자로 받으면 올해 또는 이번 달 $cal_year = $this_year+1900 if ($cal_year == 0); $cal_month = $this_month+1 if ($cal_month == 0); # 년도에 + 또는 - 가 있으면 올해로부터 변위 계산 if ($cal_year =~ /\+(\d+)/ ) { $cal_year = $this_year+1900 + $1; } elsif ($cal_year =~ /-(\d+)/ ) { $cal_year = $this_year+1900 - $1; } # 달에 + 또는 - 가 있으면 이번 달로부터 변위 계산 if ($cal_month =~ /\+(\d+)/ ) { $cal_month = $this_month+1 + $1; while ($cal_month > 12) { $cal_month -= 12; $cal_year++; } } elsif ($cal_month =~ /-(\d+)/ ) { $cal_month = $this_month+1 - $1; while ($cal_month < 1) { $cal_month += 12; $cal_year--; } } # 1902년부터 2037년 사이만 지원함. 그 범위를 벗어나면 1902년과 2037년으로 계산 $cal_year = 2037 if ($cal_year > 2037); $cal_year = 1902 if ($cal_year < 1902); # 1월~9월은 01~09로 만듦 if ($cal_month < 10) { $cal_month = "0" . $cal_month; } # 달력 제목 출력 $result .= "<TABLE class='calendar'>"; $result .= "<CAPTION class='calendar'>" ."<a href=\"$ScriptName?$toc_mainpage/$cal_year-$cal_month\">" .(length($toc_mainpage)?"$toc_mainpage<br>":"") ."$cal_year-$cal_month" ."</a>" ."</CAPTION>"; # 상단의 요일 출력 $result .= "<TR class='calendar'>"; for (0..6) { $result .= "<TH class='calendar'>" . "<span style='color:$cal_color[$_]'>$cal_dow[$_]</span></TH>"; } $result .= "</TR>"; # 인자로 주어진 달의 1일날을 찾음 $cal_time = timelocal(0,0,0,1,$cal_month-1,$cal_year); ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($cal_time); # 달력의 첫번째 날 찾음 $cal_time -= $wday * (60 * 60 * 24); ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($cal_time); # 달력 그림 my ($temp_month, $temp_day); for (1..6) { $result .= "<TR class='calendar'>"; for (0..6) { # 1~9는 01~09로 만듦 ($temp_month, $temp_day) = ($mon + 1, $mday); $temp_month = "0".$temp_month if ($temp_month < 10); $temp_day = "0".$temp_day if ($temp_day < 10); $cal_page = ($year + 1900)."-".($temp_month)."-".($temp_day); $cal_result = $mday; $span_style = ""; if (($year == $this_year) && ($mon == $this_month) && ($mday == $this_day)) { $td_class = "calendartoday"; $span_style = "text-decoration: underline; "; } else { $td_class = "calendar"; } # 해당 날짜에 포스트가 있는 경우 my ($page, $pagename) = ("", ""); if ($tocitems =~ /^\[\[(.+?)(\|.*)?\]\] $cal_page$/m) { ($page, $pagename) = ($1, $1); $page =~ s|^/|$toc_mainpage/|; $page = &FreeToNormal($page); $span_style .= "font-weight: bold; text-decoration: underline; "; $wday = 7; # 현재 보는 페이지에 해당하는 날짜인 경우 if ($page eq $OpenPageName) { $td_class .= "thispage"; } } if ($cal_month != ($mon+1)) { $span_style .= "font-size: 0.9em; "; } $result .= "<td class='$td_class'>" .(($page)?"<a href=\"$ScriptName?$page\" title=\"$pagename\">":"") ."<span style='color:$cal_color[$wday]; $span_style'>" .$cal_result ."</span>" .(($page)?"</a>":"") ."</td>"; $cal_time += (60 * 60 * 24); ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($cal_time); } $result .= "</TR>"; # 4 또는 5 줄로 끝낼 수 있으면 끝냄 last if (($mon+1 > $cal_month) || ($year+1900 > $cal_year)); } $result .= "</table>"; return $result; } 1;
# <blog_includeorder(목차페이지,시작순서,끝순서)> # 목차페이지를 읽어서 시작순서부터 끝순서까지의 페이지를 include한다. sub blog_includeorder { my ($txt) = @_; $txt =~ s/(^|\n)<blog_includeorder\(([^,]+),([-+]?\d+),([-+]?\d+)\)>([\r\f]*\n)/$1.&MacroBlogIncludeOrder($2,$3,$4).$5/geim; return $txt; } sub MacroBlogIncludeOrder { use strict; my ($tocpage, $start, $end) = @_; # 라이브러리 읽음 my ($MacrosDir, $MyMacrosDir) = ("./macros/", "./mymacros/"); if (-f "$MyMacrosDir/blog_library.pl") { require "./$MyMacrosDir/blog_library.pl"; } elsif (-f "$MacrosDir/blog_library.pl") { require "./$MacrosDir/blog_library.pl"; } else { return "<font color='red'>blog_library.pl not found</font>"; } # 목차페이지로부터 목차리스트를 얻어냄 my ($status, $toc_mainpage, @tocitem_List) = &BlogReadToc($tocpage); if (!$status) { return "$toc_mainpage"; } # 조건에 맞는 리스트를 구성 ($status, @tocitem_List) = &BlogGetListOrder($start, $end, @tocitem_List); if (!$status) { return "@tocitem_List"; } # 리스트의 각 페이지를 읽어서 include함 my $txt; my ($page, $date); foreach my $item (@tocitem_List) { if ($item =~ /^(.+)$FS1(.*)$FS1(.+)$/) { ($page, $date) = ($1, $3); } $page =~ s|^/|$toc_mainpage/|; $page = &FreeToNormal($page); $txt .= &MacroInclude($page); } return $txt; } 1;
# <blog_includeperiod(목차페이지,시작날짜,끝날짜)> # 목차페이지를 읽어서 시작날짜부터 끝날짜까지의 페이지를 include한다. sub blog_includeperiod { my ($txt) = @_; $txt =~ s/(^|\n)<blog_includeperiod\(([^,]+),([\d-]+),([\d-]+)\)>([\r\f]*\n)/$1.&MacroBlogIncludePeriod($2,$3,$4).$5/geim; return $txt; } sub MacroBlogIncludePeriod { use strict; my ($tocpage, $startdate, $enddate) = @_; # 라이브러리 읽음 my ($MacrosDir, $MyMacrosDir) = ("./macros/", "./mymacros/"); if (-f "$MyMacrosDir/blog_library.pl") { require "./$MyMacrosDir/blog_library.pl"; } elsif (-f "$MacrosDir/blog_library.pl") { require "./$MacrosDir/blog_library.pl"; } else { return "<font color='red'>blog_library.pl not found</font>"; } # 목차페이지로부터 목차리스트를 얻어냄 my ($status, $toc_mainpage, @tocitem_List) = &BlogReadToc($tocpage); if (!$status) { return "$toc_mainpage"; } # 조건에 맞는 리스트를 구성 ($status, @tocitem_List) = &BlogGetListPeriod($startdate, $enddate, @tocitem_List); if (!$status) { return "@tocitem_List"; } # 리스트의 각 페이지를 읽어서 include함 my $txt; my ($page, $date); foreach my $item (@tocitem_List) { if ($item =~ /^(.+)$FS1(.*)$FS1(.+)$/) { ($page, $date) = ($1, $3); } $page =~ s|^/|$toc_mainpage/|; $page = &FreeToNormal($page); $txt .= &MacroInclude($page); } return $txt; } 1;
# <blog_listorder(목차페이지,시작순서,끝순서[,날짜출력방식])> # 목차페이지를 읽어서 시작순서부터 끝순서까지의 페이지의 목록을 출력 sub blog_listorder { my ($txt) = @_; $txt =~ s/\&__LT__;blog_listorder\(([^,]+),([-+]?\d+),([-+]?\d+)(,([-+]?\d))?\)\&__GT__;/&MacroBlogListOrder($1,$2,$3,$5)/gei; return $txt; } sub MacroBlogListOrder { use strict; my ($tocpage, $start, $end, $showdate) = @_; # 라이브러리 읽음 my ($MacrosDir, $MyMacrosDir) = ("./macros/", "./mymacros/"); if (-f "$MyMacrosDir/blog_library.pl") { require "./$MyMacrosDir/blog_library.pl"; } elsif (-f "$MacrosDir/blog_library.pl") { require "./$MacrosDir/blog_library.pl"; } else { return "<font color='red'>blog_library.pl not found</font>"; } # 목차페이지로부터 목차리스트를 얻어냄 my ($status, $toc_mainpage, @tocitem_List) = &BlogReadToc($tocpage); if (!$status) { return "$toc_mainpage"; } # 조건에 맞는 리스트를 구성 ($status, @tocitem_List) = &BlogGetListOrder($start, $end, @tocitem_List); if (!$status) { return "@tocitem_List"; } # 리스트의 각 페이지를 목록 출력 my $txt; $txt = "<UL>"; my ($page, $pagename, $date, $pageid); foreach my $item (@tocitem_List) { if ($item =~ /^(.+)$FS1(.*)$FS1(.+)$/) { ($page, $pagename, $date) = ($1, $2, $3); } $pageid = $page; $pageid =~ s|^/|$toc_mainpage/|; $pageid = &FreeToNormal($pageid); $page = $pagename if ($pagename); if ($showdate == 0) { $txt .= "<LI>".&GetPageOrEditLink($pageid,$page); } elsif ($showdate < 0) { $txt .= "<LI>($date) ".&GetPageOrEditLink($pageid,$page); } else { $txt .= "<LI>".&GetPageOrEditLink($pageid,$page)." ($date)"; } } $txt .= "</UL>"; return $txt; } 1;
# <blog_listperiod(목차페이지,시작날짜,끝날짜[,날짜출력방식])> # 목차페이지를 읽어서 시작날짜부터 끝날짜까지의 페이지의 목록을 출력 sub blog_listperiod { my ($txt) = @_; $txt =~ s/\&__LT__;blog_listperiod\(([^,]+),([\d-]+),([\d-]+)(,([-+]?\d))?\)\&__GT__;/&MacroBlogListPeriod($1,$2,$3,$5)/gei; return $txt; } sub MacroBlogListPeriod { use strict; my ($tocpage, $startdate, $enddate, $showdate) = @_; # 라이브러리 읽음 my ($MacrosDir, $MyMacrosDir) = ("./macros/", "./mymacros/"); if (-f "$MyMacrosDir/blog_library.pl") { require "./$MyMacrosDir/blog_library.pl"; } elsif (-f "$MacrosDir/blog_library.pl") { require "./$MacrosDir/blog_library.pl"; } else { return "<font color='red'>blog_library.pl not found</font>"; } # 목차페이지로부터 목차리스트를 얻어냄 my ($status, $toc_mainpage, @tocitem_List) = &BlogReadToc($tocpage); if (!$status) { return "$toc_mainpage"; } # 조건에 맞는 리스트를 구성 ($status, @tocitem_List) = &BlogGetListPeriod($startdate, $enddate, @tocitem_List); if (!$status) { return "@tocitem_List"; } # 리스트의 각 페이지를 목록 출력 my $txt; $txt = "<UL>"; my ($page, $pagename, $date, $pageid); foreach my $item (@tocitem_List) { if ($item =~ /^(.+)$FS1(.*)$FS1(.+)$/) { ($page, $pagename, $date) = ($1, $2, $3); } $pageid = $page; $pageid =~ s|^/|$toc_mainpage/|; $pageid = &FreeToNormal($pageid); $page = $pagename if ($pagename); if ($showdate == 0) { $txt .= "<LI>".&GetPageOrEditLink($pageid,$page); } elsif ($showdate < 0) { $txt .= "<LI>($date) ".&GetPageOrEditLink($pageid,$page); } else { $txt .= "<LI>".&GetPageOrEditLink($pageid,$page)." ($date)"; } } $txt .= "</UL>"; return $txt; } 1;
# <blog_prevnext(목차페이지)> # 목차페이지를 읽어서, 현재 보고 있는 페이지의 이전 페이지와 다음 페이지의 링크를 출력 sub blog_prevnext { my ($txt) = @_; $txt =~ s/&__LT__;blog_prevnext\((.*?)\)&__GT__;/&MacroBlogPrevNext($1)/gei; return $txt; } sub MacroBlogPrevNext { use strict; my ($tocpage) = @_; my ($mainpage, $subpage); my $txt; # 라이브러리 읽음 my ($MacrosDir, $MyMacrosDir) = ("./macros/", "./mymacros/"); if (-f "$MyMacrosDir/blog_library.pl") { require "./$MyMacrosDir/blog_library.pl"; } elsif (-f "$MacrosDir/blog_library.pl") { require "./$MacrosDir/blog_library.pl"; } else { return "<font color='red'>blog_library.pl not found</font>"; } # 목차페이지로부터 목차리스트를 얻어냄 my ($status, $toc_mainpage, @tocitem_List) = &BlogReadToc($tocpage); if (!$status) { return "$toc_mainpage"; } if ($OpenPageName =~ m|(.*)/(.*)|) { ($mainpage, $subpage) = ($1,$2); } else { $mainpage = $OpenPageName; } # 목차에서 현재 페이지의 위치를 찾음 my $idx = 0; for ($idx = 0; $idx <= $#tocitem_List; $idx++) { my $line = $tocitem_List[$idx]; if ($line =~ m/\[\[$FreeLinkPattern(\|[^\]]+)?\]\]/) { my $link = $1; $link =~ s/ /_/g; if (($link eq $OpenPageName) || ($link eq "/$subpage")) { last; } } elsif ($line =~ m/$LinkPattern/) { my $link = $1; $link =~ s/ /_/g; if (($link eq $OpenPageName) || ($link eq "/$subpage")) { last; } } } # 이전페이지와 다음페이지 이름 찾음 my ($prev, $next); if ($idx > $#tocitem_List) { return ""; # return "[Not found this page:$OpenPageName in TOC]"; } my $item; my ($thispage, $thispagename, $thisdate); $item = $tocitem_List[$idx]; if ($item =~ /\[\[(.+?)(\|.*)?\]\] (\d+-\d+-\d+)/) { ($thispage, $thispagename, $thisdate) = ($1, $1, $3); $thispage =~ s|^/|$toc_mainpage/|; $thispage = &FreeToNormal($thispage); } my ($prevpage, $prevpagename, $prevdate); my ($nextpage, $nextpagename, $nextdate); if ($idx > 0) { $item = $tocitem_List[$idx-1]; if ($item =~ /\[\[(.+?)(\|.*)?\]\] (\d+-\d+-\d+)/) { ($prevpage, $prevpagename, $prevdate) = ($1, $1, $3); $prevpage =~ s|^/|$toc_mainpage/|; $prevpage = &FreeToNormal($prevpage); } } if ($idx < $#tocitem_List) { $item = $tocitem_List[$idx+1]; if ($item =~ /\[\[(.+?)(\|.*)?\]\] (\d+-\d+-\d+)/) { ($nextpage, $nextpagename, $nextdate) = ($1, $1, $3); $nextpage =~ s|^/|$toc_mainpage/|; $nextpage = &FreeToNormal($nextpage); } } # 출력 my $shortCutUrl = "$ScriptName".&ScriptLinkChar(); my $shortCutKey; $txt = "<CENTER>"; if ($prevpage) { $txt .= "<b><<</b> ". &GetPageOrEditLink($prevpage, $prevpagename). " ($prevdate)[p]"; $shortCutKey .= "key['p'] = \"${shortCutUrl}$prevpage\";\n" } $txt .= "  <b>|</b> $thispagename ($thisdate) <b>|</b> "; if ($nextpage) { $txt .= &GetPageOrEditLink($nextpage, $nextpagename). " ($nextdate)[n]". " <b>>></b>"; $shortCutKey .= "key['n'] = \"${shortCutUrl}$nextpage\";\n" } $txt .= "</CENTER>\n"; if ($UseShortcut && $shortCutKey) { $txt .= "<script>\n". "<!--\n". $shortCutKey. "-->\n". "</script>"; } return $txt; } 1;
# <blog_prevnextmonth> # 이전 달과 다음 달로 가는 링크를 만듦 sub blog_prevnextmonth { my ($txt) = @_; $txt =~ s/&__LT__;blog_prevnextmonth&__GT__;/&MacroBlogPrevNextMonth()/gei; return $txt; } sub MacroBlogPrevNextMonth { use strict; if ($OpenPageName =~ m|^(.*/)?(\d{4})-(\d{2})$|) { my ($mainpage, $year, $month) = ($1, $2, $3); my ($prev_year, $prev_month) = ($year, $month - 1); my ($next_year, $next_month) = ($year, $month + 1); if ($prev_month <= 0) { $prev_year--; $prev_month = 12; } elsif ($next_month >= 13) { $next_year++; $next_month = 1; } if ($prev_month < 10) { $prev_month = "0" . $prev_month; } if ($next_month < 10) { $next_month = "0" . $next_month; } my $prev_page = "$mainpage$prev_year-$prev_month"; my $next_page = "$mainpage$next_year-$next_month"; my $shortCutUrl = "$ScriptName".&ScriptLinkChar(); my $txt; $txt = "<CENTER>". "<b><<<</b> ". &GetPageOrEditLink($next_page). " [p]". "  <b>|</b> ".T('Monthly View')." <b>|</b> ". &GetPageOrEditLink($prev_page). " [n]". " <b>>>></b>". "</CENTER>"; $txt .= <<EOF; <script> <!-- key['p'] = "${shortCutUrl}$next_page"; key['n'] = "${shortCutUrl}$prev_page"; --> </script> EOF return $txt; } else { return ""; } } 1;
# blog_library.pl # blog_*** 매크로 시리즈들이 공통으로 사용하는 함수들을 모아 둔 라이브러리 # param: 목차페이지 # return: ($status, $page, @list); # $status : 성공하면 1, 실패하면 0 # $page : 성공하면 목차페이지의 상위페이지 이름, 실패하면 에러메시지 # @list : 목차페이지로부터 읽은 목차 리스트 sub BlogReadToc($tocpage) { use strict; my ($tocpage) = @_; # 목차페이지 이름 분석 $tocpage = &FreeToNormal(&RemoveLink($tocpage)); if (my $temp = &ValidId($tocpage)) { return (0, "<font color='red'>$temp</font>"); } my ($toc_mainpage, $toc_subpage); if ($tocpage =~ m|(.*)/(.*)|) { ($toc_mainpage, $toc_subpage) = ($1,$2); } else { $toc_mainpage = $tocpage; } # 페이지 목록 읽음 my ($fname, $status, $data); $fname = &GetPageFile($tocpage); if (!(-f $fname)) { return (0, "<font color='red'>No such page: $tocpage</font>"); } ($status, $data) = &ReadFile($fname); if (!$status) { return (0, "<font color='red'>Error in read pagefile: $tocpage</font>"); } my %temp_Page = split(/$FS1/, $data, -1); my %temp_Section = split(/$FS2/, $temp_Page{'text_default'}, -1); my %temp_Text = split(/$FS3/, $temp_Section{'data'}, -1); my $tocpage_Text = $temp_Text{'text'}; # 라인 별로 분리 my @tocpage_Lines = split('\n',$tocpage_Text); my @tocitem_List; # 유효한 라인만 추출 foreach my $line (@tocpage_Lines) { if ($line =~ m/^* (\[\[.+?\]\] \d+-\d+-\d+)\s*$/) { push(@tocitem_List, $1); } } return (1, $toc_mainpage, @tocitem_List); } # param: 시작순서, 끝순서, 목차리스트 # return: ($status, @list) # $status : 성공하면 1, 실패하면 0 # @list : 성공하면 시작순서부터 끝순서까지의 리스트. 실패하면 에러메시지 sub BlogGetListOrder { use strict; my ($start, $end, @tocitem_List) = @_; if (($start == 0) || ($end == 0)) { return (0, "<font color='red'>Invalid parameter: 0</font>"); } if ($start > 0) { $start--; } else { $start = $#tocitem_List + $start + 1; } if ($end > 0) { $end--; } else { $end = $#tocitem_List + $end + 1; } $start = 0 if ($start < 0); $start = $#tocitem_List if ($start > $#tocitem_List); $end = 0 if ($end < 0); $end = $#tocitem_List if ($end > $#tocitem_List); if ($start <= $end) { @tocitem_List = @tocitem_List[$start .. $end]; } else { @tocitem_List = reverse(@tocitem_List[$end .. $start]); } my @list; my ($page, $pagename, $date); foreach my $item (@tocitem_List) { if ($item =~ /\[\[(.+?)(\|.*)?\]\] (\d+)-(\d+)-(\d+)/) { $page = $1; $pagename = $2; $date = sprintf("%4d-%02d-%02d",$3,$4,$5); $pagename =~ s/^\|//; push(@list, "$page$FS1$pagename$FS1$date"); } } return ("1", @list); } # param: 시작날짜, 끝날짜, 목차리스트 # return: ($status, @list) # $status : 성공하면 1, 실패하면 0 # @list : 성공하면 시작날짜부터 끝날짜까지의 리스트. 실패하면 에러메시지 sub BlogGetListPeriod { use strict; my ($startdate, $enddate, @tocitem_List) = @_; if ($startdate =~ /^(\d{4})-(\d{1,2})-(\d{1,2})$/) { $startdate = sprintf("%4d%02d%02d",$1,$2,$3); } else { return (0,"<font color='red'>Invalid parameter: $startdate</font>"); } if ($enddate =~ /^(\d+)-(\d+)-(\d+)$/) { $enddate = sprintf("%4d%02d%02d",$1,$2,$3); } else { return (0,"<font color='red'>Invalid parameter: $enddate</font>"); } my @list; my ($page, $pagename, $date, $reverse); if ($startdate > $enddate) { ($startdate, $enddate, $reverse) = ($enddate, $startdate, 1); } foreach my $item (@tocitem_List) { if ($item =~ /\[\[(.+?)(\|.*)?\]\] (\d+)-(\d+)-(\d+)/) { $page = $1; $pagename = $2; $date = sprintf("%4d%02d%02d",$3,$4,$5); last if ($date < $startdate); if ($date <= $enddate) { $date = sprintf("%4d-%02d-%02d",$3,$4,$5); $pagename =~ s/^\|//; push(@list, "$page$FS1$pagename$FS1$date"); } } } @list = reverse @list if ($reverse); return (1, @list); } 1;
sub action_blog_newpost { use strict; my $id = &GetParam("id",""); my $pageid = &GetParam("pageid",""); my $title = &GetParam("title",""); my $date = &GetParam("date",""); $title =~ s/^\s*//g; $title =~ s/\s*$//g; $title = "[[/$title]]"; if ($date =~ /^(\d+)-(\d+)-(\d+)$/) { $date = sprintf("%4d-%02d-%02d",$1,$2,$3); } &OpenPage($id); &OpenDefaultText(); my $string = $Text{'text'}; $string =~ s/(<blog_newpost\($id\)>)/$1\n* $title $date/; if (!&UserCanEdit($id,1)) { $pageid = ""; } &DoPostMain($string, $id, "*", $Section{'ts'}, 0, 1, $pageid); return; } 1;
sub action_comments { use strict; my $id = &GetParam("id", ""); my $pageid = &GetParam("pageid", ""); my $name = &GetParam("name", ""); my $newcomments = &GetParam("comment", ""); my $up = &GetParam("up", ""); my ($timestamp) = CalcDay($Now) . " " . CalcTime($Now); my $string; my $long = &GetParam("long", ""); &ValidIdOrDie($id); # 블로그 지원을 위한 꽁수 my ($blogrcpage, $blogrccomment); if ($id =~ m/(.+)(\/|%2f|%2F)(.+)/) { $blogrcpage = "$1/BlogRc"; } else { $blogrcpage = "BlogRc"; } if (-f &GetPageFile($blogrcpage)) { $blogrccomment = $newcomments; } else { $blogrcpage = ""; } # thread my $threadindent = &GetParam("threadindent", ""); my $abs_up = abs($up); my ($threshold1, $threshold2) = (100000000, 1000000000); if ($newcomments =~ /^\s*$/) { &ReBrowsePage($pageid, "", 0); return; } $name = &GetRemoteHost(0) if ($name eq ""); $name =~ s/,/./g; $newcomments = &QuoteHtml($newcomments); &OpenPage($id); &OpenDefaultText(); $string = $Text{'text'}; if ($threadindent ne '') { # thread $newcomments =~ s/^\s*//g; $newcomments =~ s/\s*$//g; $newcomments =~ s/(\n)\s*(\r?\n)/$1$2/g; $newcomments =~ s/(\r?\n)/ \\\\$1/g; my ($comment_head, $comment_tail) = ("", ""); my $newup; if (($abs_up >= 100) && ($abs_up <= $threshold2)) { # 커멘트 권한 상속 $newup = $Now - $threshold2; } else { $newup = $Now; } $comment_tail = "<thread($id,$newup," . ($threadindent+1) . ")>"; if ($threadindent >= 1) { for (1 .. $threadindent) { $comment_head .= ":"; } $comment_head .= " "; } else { # 새글 # $comment_head = "<thread>\n"; # $comment_tail .= "\n</thread>"; } if (($up > 0) && ($up < $threshold1)) { # 위로 달리는 새글 $string =~ s/(\<thread\($id,$up(,\d+)?\)\>)/$comment_head$newcomments <mysign($name,$timestamp)>\n$comment_tail\n\n$1/; } else { # 리플 or 아래로 달리는 새글 $string =~ s/(\<thread\($id,$up(,\d+)?\)\>)/$1\n\n$comment_head$newcomments <mysign($name,$timestamp)>\n$comment_tail/; } } elsif ($long) { # longcomments $newcomments =~ s/^\s*//g; $newcomments =~ s/\s*$//g; $newcomments =~ s/(\n)\s*(\r?\n)/$1$2/g; $newcomments =~ s/(\r?\n)/ \\\\$1/g; if ($up > 0) { $string =~ s/(\<longcomments\($id,$up\)\>)/\n$newcomments <mysign($name,$timestamp)>\n$1/; } else { $string =~ s/(\<longcomments\($id,$up\)\>)/$1\n$newcomments <mysign($name,$timestamp)>\n/; } } else { # comments $newcomments =~ s/(----+)/<nowiki>$1<\/nowiki>/g; if ($up > 0) { $string =~ s/\<comments\($id,$up\)\>/* ''' $name ''' : $newcomments - <small>$timestamp<\/small>\n\<comments\($id,$up\)\>/; } else { $string =~ s/\<comments\($id,$up\)\>/\<comments\($id,$up\)\>\n* ''' $name ''' : $newcomments - <small>$timestamp<\/small>/; } } if (((!&UserCanEdit($id,1)) && (($abs_up < 100) || ($abs_up > $threshold2))) || (&UserIsBanned())) { # 에디트 불가 $pageid = ""; } # 블로그 지원을 위한 꽁수 if ($pageid && $blogrcpage) { $blogrccomment =~ s/(\r?\n)/ /g; $blogrccomment =~ s/\[/{/g; $blogrccomment =~ s/\]/}/g; if (length($blogrccomment) > 30) { $blogrccomment = substr($blogrccomment, 0, 27); $blogrccomment =~ s/(([\x80-\xff].)*)[\x80-\xff]?$/$1/; $blogrccomment .= "..."; } $blogrccomment = &QuoteHtml($blogrccomment); $blogrccomment =~ s/----+/---/g; $blogrccomment =~ s/^ *//; $blogrccomment = "C) $blogrccomment"; my ($fname, $status, $data); $fname = &GetPageFile($blogrcpage); if (-f $fname) { ($status, $data) = &ReadFile($fname); if ($status) { my %temp_Page = split(/$FS1/, $data, -1); my %temp_Section = split(/$FS2/, $temp_Page{'text_default'}, -1); my %temp_Text = split(/$FS3/, $temp_Section{'data'}, -1); my $blogrc_Text = $temp_Text{'text'}; my $date = &CalcDayNow(); if ($date =~ /(\d+)-(\d+)-(\d+)/) { $date = sprintf("%4d-%02d-%02d",$1,$2,$3); } if ($blogrc_Text =~ /^\*/m) { $blogrc_Text =~ s/^\*/* [[$id|$blogrccomment]] $date\n*/m; } else { $blogrc_Text .= "\n* [[$id|$blogrccomment]] $date"; } my $backup = $Section{'ts'}; &DoPostMain($blogrc_Text, $blogrcpage, "", $temp_Section{'ts'}, 0, 1, "!!"); $Section{'ts'} = $backup; } } } DoPostMain($string, $id, "*", $Section{'ts'}, 0, 0, $pageid); return; } 1;
sub action_trackback { use strict; my $id = &GetParam("id",""); my $normal_id = $id; my $url = &GetParam('url'); my $title = &GetParam('title', $url); my $blog_name = &GetParam('blog_name'); my $excerpt = &GetParam('excerpt'); # 블로그 지원을 위한 꽁수 my ($blogrcpage, $blogrccomment); if ($id =~ m/(.+)(\/|%2f|%2F)(.+)/) { $blogrcpage = "$1/BlogRc"; } else { $blogrcpage = "BlogRc"; } if (-f &GetPageFile($blogrcpage)) { $blogrccomment = $excerpt; } else { $blogrcpage = ""; } if (length($excerpt) > 255) { $excerpt = substr($excerpt, 0, 252); $excerpt =~ s/(([\x80-\xff].)*)[\x80-\xff]?$/$1/; $excerpt .= "..."; } $excerpt =~ s/(\r?\n)/ /g; $excerpt = &QuoteHtml($excerpt); $excerpt = "<nowiki>$excerpt</nowiki>"; if ($FreeLinks) { $normal_id = &FreeToNormal($id); } if ($url eq '') { &SendTrackbackResponse("1", "No URL (url)"); } elsif ($id eq '') { &SendTrackbackResponse("1", "No Pagename (id)"); } elsif (!&PageCanReceiveTrackbackPing($normal_id)) { &SendTrackbackResponse("1", "Invalid Pagename (Page is missing, or Trackback is not allowed)"); } elsif (my $bannedText = &TextIsBanned($blog_name.$url.$title.$excerpt)) { &SendTrackbackResponse("1", "[$bannedText] is a Banned text"); } else { &OpenPage($normal_id); &OpenDefaultText(); my $string = $Text{'text'}; my $macro = "\<trackbackreceived\>"; if (!($string =~ /$macro/)) { $string .= "\n$macro\n"; } my $timestamp = &CalcDay($Now) . " " . &CalcTime($Now); my $newtrackbackreceived = "* " . &Ts('Trackback from %s', "'''<nowiki>$blog_name</nowiki>'''") . " $timestamp\n" . "** " . &T('Title:') . " [$url $title]\n" . "** " . &T('Content:') . " $excerpt"; $string =~ s/($macro)/$newtrackbackreceived\n$1/; # 블로그 지원을 위한 꽁수 if ($blogrcpage) { $blogrccomment =~ s/(\r?\n)/ /g; $blogrccomment =~ s/\[/{/g; $blogrccomment =~ s/\]/}/g; if (length($blogrccomment) > 30) { $blogrccomment = substr($blogrccomment, 0, 27); $blogrccomment =~ s/(([\x80-\xff].)*)[\x80-\xff]?$/$1/; $blogrccomment .= "..."; } $blogrccomment = &QuoteHtml($blogrccomment); $blogrccomment =~ s/----+/---/g; $blogrccomment =~ s/^ *//; $blogrccomment = "T) $blogrccomment"; my ($fname, $status, $data); $fname = &GetPageFile($blogrcpage); if (-f $fname) { ($status, $data) = &ReadFile($fname); if ($status) { my %temp_Page = split(/$FS1/, $data, -1); my %temp_Section = split(/$FS2/, $temp_Page{'text_default'}, -1); my %temp_Text = split(/$FS3/, $temp_Section{'data'}, -1); my $blogrc_Text = $temp_Text{'text'}; my $date = &CalcDayNow(); if ($date =~ /(\d+)-(\d+)-(\d+)/) { $date = sprintf("%4d-%02d-%02d",$1,$2,$3); } if ($blogrc_Text =~ /^\*/m) { $blogrc_Text =~ s/^\*/* [[$id|$blogrccomment]] $date\n*/m; } else { $blogrc_Text .= "\n* [[$id|$blogrccomment]] $date"; } my $backup = $Section{'ts'}; &DoPostMain($blogrc_Text, $blogrcpage, "", $temp_Section{'ts'}, 0, 1, "!!"); $Section{'ts'} = $backup; } } } &DoPostMain($string, $id, &T('New Trackback Received'), $Section{'ts'}, 0, 0, "!!"); &SendTrackbackResponse(0, ""); } } sub SendTrackbackResponse { my ($code, $message) = @_; if ($code == 0) { print <<END; Content-Type: application/xml; charset: iso-8859-1\n <?xml version="1.0" encoding="iso-8859-1"?> <response> <error>0</error> </response> END } else { print <<END; Content-Type: application/xml; charset: iso-8859-1\n <?xml version="1.0" encoding="iso-8859-1"?> <response> <error>$code</error> <message>$message</message> </response> END } } 1;
# <blog_rss(목차페이지[,블로그페이지])> # XML아이콘과 RSS의 URL 링크를 출력 sub blog_rss { my ($txt) = @_; $txt =~ s/\&__LT__;blog_rss\((.*?)\)&__GT__;/&MacroBlogRss($1)/gei; $txt =~ s/&__LT__;blog_rss&__GT__;.*?&__LT__;\/blog_rss&__GT__;//geis; return $txt; } sub MacroBlogRss { use strict; my ($arg) = @_; my $txt; my ($listpage, $blogpage); if ($arg =~ /^([^,]+)(,([^,]+))?$/) { ($listpage, $blogpage) = ($1, $3); } else { return "<font color='red'>Invalid args</font>"; } my $blogname; $listpage = &RemoveLink($listpage); $blogname = $listpage; $listpage = &FreeToNormal($listpage); $blogpage = &RemoveLink($blogpage); $blogname = $blogpage if ($blogpage); $blogpage = &FreeToNormal($blogpage); $txt = &ScriptLink("action=blog_rss&listpage=$listpage&blogpage=$blogpage", "<img align='absmiddle' src='$IconDir/xml_rss.gif'> Get RSS of $blogname"); return $txt; } 1;
# blog_rss 액션 my @ChannelField = ('title','link','description','language', 'copyright','manageingEditor','webMaster','pubDate', 'lastBuildDate','category','generator','docs','could', 'ttl','image','rating','textInput','skipHours','skipDays'); my @ItemField = ('title','link','description','author','category', 'comments','enclosure','guid','pubDate','source'); my %NeedCdata = map { $_ => 1 } ('description'); # CDATA 로 묶어 줘야 할 필드 my (%RssChannelField, %RssItemFieldInList, %RssItemField, $ListPageAuthor); sub action_blog_rss { use strict; my $listpage = &GetParam("listpage",""); # 참조할 목차 페이지 my $blogpage = &GetParam("blogpage",""); # RSS의 link항목에 들어갈 페이지 my $num_items = &GetParam("items",15); # xml파일의 item 갯수 my $xml = ""; my $cachefile = $listpage."__".$blogpage."__".$num_items; $cachefile =~ s/(\W)/uc sprintf "_%02x", ord($1)/eg; $cachefile = "$TempDir/rss_$cachefile.xml"; if (-f $cachefile) { # cache 파일의 마지막 수정 시각 이후에 사이트에 변동이 없는 경우 # cache 파일을 읽어서 출력 my $cache_mtime = (stat($cachefile))[9]; my $rclog_mtime = (stat($RcFile))[9]; if ($cache_mtime > $rclog_mtime) { my ($status, $data) = &ReadFile($cachefile); if ($status) { $xml = $data; } } } if ($xml eq "") { # xml을 새로 생성함 my ($rssHeader, $rssBody, $rssFooter); # 채널 정보의 디폴트 값을 먼저 설정 # 사이트 제목 $RssChannelField{'title'} = &QuoteHtml($SiteName); # 사이트 링크 $FullUrl = $q->url(-full=>1) if ($FullUrl eq ""); $QuotedFullUrl = &QuoteHtml($FullUrl); $RssChannelField{'link'} = $QuotedFullUrl; $RssChannelField{'link'} .= &ScriptLinkChar() . $blogpage if ($blogpage); # 사이트 설명 $RssChannelField{'description'} = $SiteDescription; # 언어 - how to detect? $RssChannelField{'language'} = "ko"; # xml작성 시각 $RssChannelField{'pubDate'} = &BlogRssGetPubDate($Now); # 리스트 페이지에 사용자가 정의한 값을 읽어서 덮어 씀 &OpenPage($listpage); &OpenDefaultText; $ListPageAuthor = $Section{'username'}; &BlogRssGetUserDefinedValue($Text{'text'}, "list"); # rss header 생성 $rssHeader = <<EOF; <?xml version="1.0" encoding="$HttpCharset" ?> <rss version="2.0"> <channel> EOF foreach my $field (@ChannelField) { if ($RssChannelField{$field} ne "") { $rssHeader .= "<$field>". (($NeedCdata{$field})?("<![CDATA[".$RssChannelField{$field}."]]>"):($RssChannelField{$field})). "</$field>". "\n"; } } # rss footer 생성 $rssFooter = <<EOF; </channel> </rss> EOF # header와 footer사이의 body 생성 $rssBody = &BlogRssGetItems($listpage, $num_items); # 전체 xml 생성 $xml = $rssHeader. $rssBody. $rssFooter; # cache 파일에 저장 &WriteStringToFile($cachefile, $xml); } # 최종 출력 print "Content-type: text/xml\n\n"; print $xml; return; } # param: 목차페이지, 아이템 갯수 # return: $txt # $txt : Rss 파일의 <item>...</item>항목들 sub BlogRssGetItems { use strict; my ($tocpage, $num_items) = @_; # 라이브러리 읽음 my ($MacrosDir, $MyMacrosDir) = ("./macros/", "./mymacros/"); if (-f "$MyMacrosDir/blog_library.pl") { require "./$MyMacrosDir/blog_library.pl"; } elsif (-f "$MacrosDir/blog_library.pl") { require "./$MacrosDir/blog_library.pl"; } else { return ""; } # 목차페이지로부터 목차리스트를 얻어냄 my ($status, $toc_mainpage, @tocitem_List) = &BlogReadToc($tocpage); if (!$status) { return ""; } # 조건에 맞는 리스트를 구성 ($status, @tocitem_List) = &BlogGetListOrder(1, $num_items, @tocitem_List); if (!$status) { return ""; } # 리스트의 각 페이지를 읽어서 item 형식으로 만들어 반환함 my $txt; my ($page, $pagename, $date, $pageid); foreach my $item (@tocitem_List) { if ($item =~ /^(.+)$FS1(.*)$FS1(.+)$/) { ($page, $pagename, $date) = ($1, $2, $3); } $pageid = $page; $pageid =~ s|^/|$toc_mainpage/|; $pageid = &FreeToNormal($pageid); # 페이지가 존재하지 않으면 통과 next if (not -f &GetPageFile($pageid)); # 아이템 필드 초기화 %RssItemField = %RssItemFieldInList; # 아이템 정보의 디폴트 값을 먼저 설정 &OpenPage($pageid); &OpenDefaultText(); # 제목 $RssItemField{'title'} = $page; # 링크 $RssItemField{'link'} = $QuotedFullUrl.&ScriptLinkChar().&EncodeUrl($pageid); # 내용 my $description = $Text{'text'}; $description =~ s/<noinclude>.*?<\/noinclude>//igs; $description =~ s/<blog_rss>.*?<\/blog_rss>//igs; $description =~ s/\n/<br \/>\n/g; $RssItemField{'description'} = $description; # 작성자 - 목차 페이지에 명시되어 있으면 그 값 사용. # 없으면 목차 페이지의 마지막 수정자의 아이디를 사용. if (not $RssItemField{'author'}) { $RssItemField{'author'} = $ListPageAuthor; } # 카테고리 - 대책없음 # $RssItemField{'category'} = ""; # 작성시각 $RssItemField{'pubDate'} = &BlogRssGetPubDate($Page{'tscreate'}); # 포스트 페이지에 사용자가 정의한 값을 읽어서 덮어 씀 &BlogRssGetUserDefinedValue($Text{'text'}); $txt .= "<item>\n"; foreach my $field (@ItemField) { if ($RssItemField{$field} ne "") { $txt .= "<$field>". (($NeedCdata{$field})?"<![CDATA[".$RssItemField{$field}."]]>":$RssItemField{$field}). "</$field>". "\n"; } } $txt .= "</item>\n"; } return $txt; } # param: timestamp값 # return: $pubDate # $pubDate : <pubDate>항목 안에 들어갈 날짜와 시각 포맷 sub BlogRssGetPubDate { my ($ts) = @_; my @dow = ("Sun","Mon","Tue","Wed","Thu","Fri","Sat"); my @month = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"); my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime($ts); my $pubDate = sprintf("%3s, %02d %3s %04d %02d:%02d:%02d +%02d00", $dow[$wday], $mday, $month[$mon], $year+1900, $hour, $min, $sec, $RssTimeZone); return $pubDate; } # param: 텍스트[,출처] # 텍스트에서 <blog_rss> </blog_rss> 부분을 찾아서 파싱하여 전역변수에 저장 sub BlogRssGetUserDefinedValue { my ($text, $where) = @_; my ($text_channel, $text_item); my $text_blog; while ($text =~ /<blog_rss>(.+?)<\/blog_rss>/igs) { $text_blog .= $1; } while ($text_blog =~ /<channel>(.+?)<\/channel>/igs) { $text_channel .= $1; } while ($text_channel =~ s/<item>(.+?)<\/item>//igs) { $text_item .= $1; } while ($text_channel =~ /<(.+?)>(.+?)<\/\1>/gs) { $RssChannelField{$1} = $2; } while ($text_item =~ /<(.+?)>(.+?)<\/\1>/gs) { if ($where eq "list") { $RssItemFieldInList{$1} = $2; } else { $RssItemField{$1} = $2; } } } 1;
ext1.82c - blog_prevnextmonth 매크로 추가.
ext1.82d - /Comments매크로와 /TrackBack 코드를 고쳐서, 코멘트나 트랙백이 달릴 때 그 내역을 기록하게 함.
ext1.83 - blog_rss 매크로, blog_rss 액션 추가
ext1.83a - 사용자가 RSS출력을 제어할 수 있게 함.
ext1.90 - /매크로파라메터에이중대괄호허용함
ext2.1b - blog_includeperiod나 blog_listperiod에서, 시작날짜와 끝날짜를 거꾸로 주면 출력순서도 반대가 되게 함
ext2.1d - blog_includeperiod, blog_includeorder 매크로는 다른 include시리즈처럼 한 줄에 그 매크로만 있고 좌우에 다른 문자나 공백이 없을 때만 동작하게 함.
1. 각 글의 description 항목에서, 태터와 이글루스를 보면
<item> <title>글제목</title> <link>글주소</link> <description><![CDATA[ 글내용]]><description> ... </item>위와 같이 "<![CDATA[ "와 "]]>"로 내용을 묶던데 이게 무슨 의미인지요? 꼭 필요한 건가요? 다른 사이트를 보면 이게 없는 곳도 있고...
2. 1을 쓰는 동안 잊어버렸습니다 -_-;;
description 뿐 아니라 title, link, pubDate 등 URL이나 날짜 항목도 CDATA로 묶어도 되는 걸까요? 지금 소스를 좀 고치고 있는데 일부 필드만 CDATA를 쓰고 일부 필드는 그렇지 않게 하려니 너무 지저분해지네요.
으음... 온갖 편법과 꽁수가 난무하고 소스도 무지 늘어났지만... 저자동고유연성의 극치를 달리는 블로그 기능이로군요. 한 위키 안에 여러 블로그를 두고 각각 RSS를 따로 제공할 수 있습니다. 그리고 모듈화 덕분에 wiki.pl도 거의 고치지 않고 되었으니 뭐... 이만하면 만족.