#!/usr/bin/perl
# はてなダイアリーの csv を XHatenaML(仮) に変換する。
#BUGS:
# - 自動リンク関係未対応。
# - pre 関係未対応。
# - 定義リスト(dl/dt/dd) 関係未対応。
# - 脚注関係未対応。
# - img, br, hr 等を /> で閉じてない。
# - 特殊文字の自動エスケープしてない(今のところ p 内の & だけ)。
# - タグによって自動 p 変換しない機能に未対応。
# - >< による自動 p 変換 on/off がはてなと非互換(不正な?使用法の場合)。
#
# - サーバで抽出されたキーワードは変換しない。
# - サーバが記録した del,ins の datetime は変換しない。
# - コメントを処理しない。
# - 添付画像を処理しない。
use strict;
my $version = "0.0";
my $usage =
"csv2xml4hatena.pl [options] file\n"
. " -row m[-n]: 指定された行の範囲を出力。\n"
. " -day YYYY-MM-DD: 指定日付のみ出力。\n"
. " -new YYYY-MM-DD: 指定日付より新しい日付のみ出力。\n"
. " -t: 入力ファイルは(CSV形式ではなく)テキスト形式(1日分)。\n"
. " -e: 入力エンコーディングの指定\n"
;
# オプション用
my @row_range = ();
my $day = "";
my $new = "";
my $text_mode = 0;
my $encoding = "shift_jis";
while ($ARGV[0] =~ /^-/) {
$_ = shift @ARGV;
if (/^-row$/) {
@row_range = &get_range(shift @ARGV);
} elsif (/^-day$/) {
$day = shift @ARGV;
} elsif (/^-new$/) {
$new = shift @ARGV;
} elsif(/^-t$/) {
$text_mode = 1;
} elsif(/^-e$/) {
$encoding = shift @ARGV;
} elsif(/^-/) {
&usage("unknown option $_");
}
}
&print_header();
if ($text_mode) {
&parse_text();
} else {
&parse_csv();
}
&print_footer();
exit;
##########################################################################
## オプション関係
sub get_range() {
my($exp) = @_;
$exp =~ /^([0-9]+)(?:-([0-9]+))?$/;
my @range = ($1, $2);
&usage("error: invalid range expression.") if ($1 eq "");
if ($2 eq "") {
$range[1] = $range[0];
}
&usage("error: invalid range expression.") if($range[1] < $range[0]);
return @range;
}
sub usage() {
print $_[0] . "\n";
print $usage;
exit(1);
}
## ヘッダ・フッタ
sub print_header() {
print <<_XML_;
_XML_
;
}
sub print_footer() {
print <<_XML_;
_XML_
;
}
##########################################################################
## 日記の解析・変換
# インデント用空白文字列
my $indent = "";
# 日記の解析
sub handle_record() {
my ($date, $title, $body, $comment, $text) = @_;
#print "$date\n";
print "\n";
&indent();
print $indent . "\n";
if ($title ne "") {
&indent();
print $indent . "$title\n";
&unindent();
}
&handle_sections($text);
print $indent . "\n";
&unindent();
}
# セクション解析用コンテクスト変数
my $annon_sect; # 匿名セクションフラグ
my @list_stack; # ul/ol/li のネスト履歴スタック
my $xl_level; # ul/ol のネストレベル
my $dlist; # 定義リストフラグ
my $auto_p; # 自動的に p にするフラグ
my $super_pre; # super pre 内
sub handle_sections() {
my ($text) = @_;
$annon_sect = 1;
@list_stack = ();
$xl_level = 0;
$dlist = 0;
$auto_p = 1;
$super_pre = 0;
foreach (split(/\x0D\x0A|[\x0D\x0A]/, $text)) {
if (!/^[-+]+[^-+]/) { # リスト以外
&clear_list_context();
}
if (/^><.+/) { # 自動 p 抑止
$auto_p = 0;
s/^>//; # > を消す
}
if (/^\*((?:\[[^\[\]]+\])*)\s*([^\[].*)$/) { # セクション
my $cat = $1;
my $title = $2;
if ($annon_sect) {
$annon_sect = 0;
} else {
&clear_section_context();
}
&start_section($cat, $title);
} elsif (/^([-+]+)([^-+].*)$/) { # リスト
my $lv = $1;
my $content = $2;
my $lvdiff = length($lv) - $xl_level;
$lv = substr($lv, $xl_level);
if ($lvdiff <= 0) {
&clear_list_context(-$lvdiff);
} else {
foreach (1..$lvdiff) {
&start_list(substr($lv, $_-1, 1));
}
}
&start_li($content);
} elsif (/^>>/) { # 引用開始(>>)
&start_bq();
} elsif (/^<) { # 引用終了(<<)
&end_bq();
} elsif (/^>\|\|/) { # super pre 開始(>||)
&start_spre();
$super_pre = 1;
} elsif (/^\|\|) { # super pre 終了(||<)
&end_spre();
$super_pre = 0;
} else {
if ($super_pre) {
print "$_\n";
} elsif ($auto_p) {
&handle_p($_);
} else {
if (/><$/) { # 自動 p 抑止を解除
$auto_p = 1;
s/<$//;
}
&indent();
print $indent . "$_\n";
&unindent();
}
}
}
&clear_list_context();
&clear_section_context();
}
sub handle_p() {
my ($p) = @_;
return if($p eq "");
$p =~ s/&/&/g;
&indent();
print $indent . "
";
print "$p";
print "
\n";
&unindent();
}
## セクション関係
sub start_section() {
my ($cat, $title) = @_;
&indent();
print "$indent\n";
&indent();
print $indent . "$title\n";
&handle_categories($cat) if ($cat ne "");
print "\n";
&unindent();
}
sub end_section() {
print $indent . "\n";
&unindent();
}
sub clear_section_context() {
if (!$annon_sect) {
&end_section();
}
}
sub handle_categories() {
my ($cat) = @_;
chop $cat;
$cat =~ s/\[//g;
my @cats = split(/\]/, $cat);
print $indent;
foreach (@cats) {
print "$_";
}
print "\n";
}
## リスト関係
sub in_li() {
return $list_stack[$#list_stack] eq "li";
}
sub start_list() {
my ($c) = @_;
my $xl = ($c eq "+") ? "ol" : "ul";
print "\n" if (&in_li());
push @list_stack, $xl;
$xl_level++;
&indent();
print $indent . "<$xl>\n";
}
sub end_list() {
my $xl = pop @list_stack;
print $indent . "$xl>\n";
&unindent();
$xl_level--;
}
sub clear_list_context() {
my $level = $xl_level;
return if($level == 0);
($level) = @_ if (1 == @_);
&end_li("") if(&in_li());
foreach (1..$level) {
&end_list();
&end_li($indent) if(&in_li());
}
}
sub start_li() {
my ($content) = @_;
push @list_stack, "li";
&indent();
$content =~ s/&/&/g;
print $indent . "$content";
}
sub end_li() {
my ($indent) = @_;
print $indent . "\n";
&unindent();
pop @list_stack;
}
## 引用関係
sub start_bq() {
my ($cite, $title) = @_;
&indent();
print $indent . "\n";
}
sub end_bq() {
print $indent . "
\n";
&unindent();
}
## super pre
sub start_spre() {
print $indent . "\n";
}
## インデント
sub indent() {
$indent .= " ";
}
sub unindent() {
$indent =~ s/ $//;
}
## テキスト用
sub parse_text() {
my $text = "";
while (<>) {
$text .= $_;
}
&handle_record("0001-01-01", "(untitled)", "", "", $text);
}
## CSV パーサ
sub parse_csv() {
for(my $row=0; my $line = <>; $row++) {
my ($date) = $line =~ /^([^,]+),/;
while (($line =~ tr/\"//) % 2 and !eof()) { # 複数行レコード読み込み
$line .= <>;
}
if ($row == 0) {
next;
} elsif (@row_range == 2) {
next if ($row < $row_range[0]);
last if ($row_range[1] < $row);
} elsif ($day ne "") {
next if ($date ne $day);
last if ($date lt $day);
} elsif ($new ne "") {
last if ($date le $new);
}
chop ($line);
$line =~ s/[\x0D]$//;
$line .= ",";
my @record = map {
s/^\"|\"$//g;
s/\"\"/\"/g;
$_;
} ($line =~ /(\"[^\"]*(?:\"\"[^\"]*)*\"|[^,]*),/g);
&handle_record(@record);
}
}