編集(管理者用) | 編集 | 差分 | 新規作成 | 一覧 | RSS | 表紙 | 検索 | 更新履歴

自力でWikiの変換処理を作ってみる

WikiフォーマットテキストをHTMLへ変換する処理を、自力で作ってみる試み。

動機

YukiWikiでは不用意に改行を入れると中身が改行のみの段落(<p>\n</p>)ができてしまう、というところに不満を持っていたのですが、それを改良しようと思ってリストを見たところ、処理が複雑でさっぱり分からなかったので、もっと(自分にとって)分かりやすい方法で変換処理を作れないか、と思ったのがはじまり。

方法

テキストエディタの置換機能を使ってテキストをHTMLに整形していく要領で、元テキストをひとつの文字列と見なし、正規表現による置換を幾重にもかけています。

例えば段落の場合はこんな感じで。

===============
PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1

PAR2 PAR2 PAR2 PAR2
PAR2 PAR2 PAR2 PAR2
===============

↓  ※取り敢えず行ごとにpでマーク付け。空行は除く。

===============
<p>PAR1 PAR1 PAR1 PAR1</p>
<p>PAR1 PAR1 PAR1 PAR1</p>
<p>PAR1 PAR1 PAR1 PAR1</p>

<p>PAR2 PAR2 PAR2 PAR2</p>
<p>PAR2 PAR2 PAR2 PAR2</p>
===============

↓  ※ "</p>\n<p>" を "\n" に置換

===============
<p>PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1</p>

<p>PAR2 PAR2 PAR2 PAR2
PAR2 PAR2 PAR2 PAR2</p>
===============

できたスクリプトは以下を参照。

変換の特徴

YukiWikiのフォーマットをサポートしていますが、変換結果は多少異なります。

input text:
===============
PARA PARA PARA


PARA PARA PARA
===============

YukiWiki 2.1.2:
===============
<p>
PARA PARA PARA
</p>
<p>
</p>
<p>
PARA PARA PARA
</p>
===============

徒委記式:
===============
<p>PARA PARA PARA</p>


<p>PARA PARA PARA</p>
===============
input text:
===============
*HEADER
PARA PARA PARA
----
PARA PARA PARA
===============

YukiWiki 2.1.2:
===============
<h2><a name="i0"> </a>HEADER</h2>
PARA PARA PARA
<hr>
PARA PARA PARA
===============
# PARAの部分が段落とならない。

徒委記式:
===============
<h2><a name="i0"> </a>HEADER</h2>
<p>PARA PARA PARA</p>
<hr>

<p>PARA PARA PARA</p>
===============
input text:
===============
-LEVEL1
--LEVEL2
---LEVEL3
-LEVEL1
--LEVEL2
--LEVEL2
-LEVEL1
===============

YukiWiki 2.1.2:
===============
<p>
</p>
<ul>
<li>LEVEL1</li>
<ul>
<li>LEVEL2</li>
<ul>
<li>LEVEL3</li>
</ul>
</ul>
<li>LEVEL1</li>
<ul>
<li>LEVEL2</li>
<li>LEVEL2</li>
</ul>
<li>LEVEL1</li>
</ul>
===============
# ulの直下にulが出現してしまっている。

徒委記式:
===============
<ul>
<li>LEVEL1<ul><li>LEVEL2<ul><li>LEVEL3</li></ul></li></ul></li>
<li>LEVEL1<ul><li>LEVEL2</li><li>LEVEL2</li></ul></li>
<li>LEVEL1</li>
</ul>
===============
# ソースの見かけはいまいちですが。
input text:
===============
-LEVEL1
---LEVEL3?
---LEVEL3?
===============

YukiWiki 2.1.2:
===============
<p>
</p>
<ul>
<li>LEVEL1</li>
<ul>
<ul>
<li>LEVEL3?</li>
<li>LEVEL3?</li>
</ul>
</ul>
</ul>
===============

徒委記式:
===============
<ul>
<li>LEVEL1<ul><li>LEVEL3?</li><li>LEVEL3?</li></ul></li>
</ul>
===============
# LEVEL3? の部分が2階層目のリストとなっていますが、
# これはこれで適切なのではないかと思ったり。
input text:
===============
>LEVEL1
>>>LEVEL3?
>>>LEVEL3?
===============

YukiWiki 2.1.2:
===============
<p>
</p>
<blockquote>
LEVEL1
<blockquote>
<blockquote>
LEVEL3?
LEVEL3?
</blockquote>
</blockquote>
</blockquote>
===============

徒委記式:
===============
<blockquote>
<p>LEVEL1</p>
<blockquote>
<p>LEVEL3?
LEVEL3?</p>
</blockquote>
</blockquote>
===============
# blockquote要素内の文字は
# 通常は段落(p要素)となります
input text:
===============
-LEVEL1
--LEVEL2
(LEVEL2?)
-LEVEL1
===============

YukiWiki 2.1.2:
===============
<p>
</p>
<ul>
<li>LEVEL1</li>
<ul>
<li>LEVEL2</li>
(LEVEL2?)
</ul>
<li>LEVEL1</li>
</ul>
===============
# ブラウザ表示では一続きのリストのように見えますが、
# ul直下にテキストが書かれているため
# 妥当なHTMLではありません。

徒委記式:
===============
<ul>
<li>LEVEL1<ul><li>LEVEL2</li></ul></li>
</ul>
<p>(LEVEL2?)</p>
<ul>
<li>LEVEL1</li>
</ul>
===============

試してみて

2004年6月2日: 変換速度がオリジナルより遅くなっていないか気になっていたのですが、手もとの環境で比較してみたところ同じ程度(やや速めかも)だったので、ここ(徒委記)に取り入れることを前向きに考えてみます。

2004年6月6日: さらに改良し、徒委記に導入しました。

2004年6月7日: 引用文(blockquote)内にリスト(ul)・定義文(dl)・表(table)・整形済み文(pre)を入れられるよう拡張。

サンプルスクリプト

# text2html.pl
# 最終更新: 2004-06-08T02:58:00+09:00

use strict;
use Benchmark;

print &main;
#timethis(3000, "&main;");

sub main {
	return &text_to_html(<<__EOD__, toc=>1);
*HEADER1

**HEADER1-1

-ITEM1
-ITEM2
-ITEM3

PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 ''BOLD''  PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1

PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2
PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 '''ITALIC'''   PAR2 PAR2 PAR2
PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2

**HEADER1-2

:TERM1:DESCRIPTION1 AND ''BOLD''

PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 ''BOLD''  PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1

:TERM2:DESCRIPTION2
:TERM3:DESCRIPTION3

----

*HEADER2

**HEADER2-1

http://www.hyuki.com/

**HEADER2-2

PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1
PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 '''''BOLD ITALIC'''''    PAR1
PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1 PAR1

>PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2
>PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 '''ITALIC'''   PAR2 PAR2 PAR2
>PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2 PAR2

LEVEL0 LEVEL0 LEVEL0 LEVEL0 LEVEL0 LEVEL0 LEVEL0

>LEVEL1
>LEVEL1
>LEVEL1
>>LEVEL2
>>LEVEL2
>>LEVEL2
>>>LEVEL3

-HELLO-1
--HELLO-2
--HELLO-2
---HELLO-3

--HELLO-2?
---HELLO-3?
---HELLO-3?
--HELLO-2?
(HELLO-2?)
--HELLO-2?

>>>LEVEL3?
>>>LEVEL3?
__EOD__
}

sub text_to_html {
    my ($arg, %option) = @_;
    my $text = '';
    my $in_verbatim_hard = 0;

    $arg =~ s/\x0D\x0A/\n/g;
    $arg =~ tr/\x0D\x0A/\n\n/;

    foreach my $line (split(/\n/, $arg)) {
        $in_verbatim_hard = 1 if ($line =~ /^---\($/);
        $in_verbatim_hard = 0 if ($line =~ /^---\)$/);
        if ($in_verbatim_hard) {
            $text .= &escape($line);
        } elsif ($line =~ /^(>{1,3})(.*)/) {
            $text .= $1 . &inline($2);
        } else {
            $text .= &inline($line);
        }
        $text .= "\n";
    }
    undef $arg;    # 以後使わないので消去

    # ブロック要素変換
    &verbatim(\$text);        # 最初に処理
    &horizontal_rule(\$text); # リストの前に処理
    &headding(\$text);
    &quotation(\$text);       # 他のブロック要素の前に処理
    &unordered_list(\$text);
    &difinition(\$text);
    &make_table(\$text);
    &preformat(\$text);
    #&block_plugin(\$text);
    &paragraph(\$text);       # 最後から2番目に処理
    &line_break(\$text);      # 最後に処理

    # 目次追加
    &insert_toc(\$text) if ($option{toc});

    return $text;
}

# verbatim機能
sub verbatim {
    my $ref = shift;
    ${$ref} =~ s{^(---?)\(\n(.+?)\n\1\)$}{
        q{<pre class="verbatim-}
        . (($1 eq '---') ? 'hard' : 'soft')
        . q{">}
        . join('<!--break-->', split(/\n/, $2))
        . '</pre>'
    }emsg;
}

# 水平線
sub horizontal_rule {
    my $ref = shift;
    ${$ref} =~ s{^----}{<hr>\n}mg;
}

# 見出し
sub headding {
    my $ref = shift;
    ${$ref} =~ s{^\*\*\*(.+)}{<h4>$1</h4>}mg;
    ${$ref} =~ s{^\*\*(.+)}{<h3>$1</h3>}mg;
    ${$ref} =~ s{^\*(.+)}{<h2>$1</h2>}mg;
}

# 引用
sub quotation {
    my $ref = shift;
    ## 3段
    ${$ref} =~ s{^>>>(.*)}{<blockquote>$1</blockquote>}mg;
    ${$ref} =~ s{</blockquote>\n<blockquote>}{<!--bqbr-->}g;
    ${$ref} =~ s{^(>.*)\n<blockquote>}{$1<blockquote>}mg;
    ## 2段
    ${$ref} =~ s{^>>(.*)}{<blockquote>$1</blockquote>}mg;
    ${$ref} =~ s{</blockquote>\n<blockquote>}{<!--bqbr-->}g;
    ${$ref} =~ s{^(>.*)\n<blockquote>}{$1<blockquote>}mg;
    ## 1段
    ${$ref} =~ s{^>(.*)}{<blockquote>$1</blockquote>}mg;
    ${$ref} =~ s{</blockquote>\n<blockquote>}{<!--bqbr-->}g;
    # 引用文中に他のブロック要素を含めるための処理
    ${$ref} =~ s{(</?blockquote>)(.)}{$1\n$2}g;
    ${$ref} =~ s{(.)(</?blockquote>)}{$1\n$2}g;
    ${$ref} =~ s{<!--bqbr-->}{\n}g;
}

# 順不同リスト
sub unordered_list {
    my $ref = shift;
    ## 3段
    ${$ref} =~ s{^---(.*)}{<ul><li>$1</li></ul>}mg;
    ${$ref} =~ s{</ul>\n<ul>}{}g;
    ${$ref} =~ s{^-(.*)\n<ul>}{-$1<ul>}mg;
    ## 2段
    ${$ref} =~ s{^--(.*)}{<ul><li>$1</li></ul>}mg;
    ${$ref} =~ s{</ul>\n<ul>}{}g;
    ${$ref} =~ s{^-(.*)\n<ul>}{-$1<ul>}mg;
    ## 1段
    ${$ref} =~ s{^-(.*)}{<ul>\n<li>$1</li>\n</ul>}mg;
    ${$ref} =~ s{</ul>\n<ul>\n}{}g;
}

# 定義
sub difinition {
    my $ref = shift;
    ${$ref} =~ s{^:([^:]+):(.+)}{<dl><dt>$1</dt><dd>$2</dd></dl>}mg;
    ${$ref} =~ s{</dl>\n<dl>}{\n}g;
}

# 表
sub make_table {
    my $ref = shift;
    ${$ref} =~ s{^,(.+)}{
        "<table>\n<tr><td>"
        . join('</td><td>', split(/,/, $1))
        . "</td></tr>\n</table>"
    }emg;
    ${$ref} =~ s{</table>\n<table>\n}{}g;
    ${$ref} =~ s{<td>\s+(.+?)\s*</td>}{<th>$1</th>}g;
}

# 整形済みテキスト
sub preformat {
    my $ref = shift;
    ${$ref} =~ s{^ (.+)}{<pre> $1</pre>}mg;
    ${$ref} =~ s{</pre>\n<pre>}{<!--break-->}g;
}

# ブロック型プラグイン
#sub block_plugin {
#    my $ref = shift;
#    ${$ref} =~ s{^#(\w+)(\((.*)\))?}{
#        my $original = "#$1$2";
#        my $plugin_name = $1;
#        my $argument = &escape($3);
#        my $result = $plugin_manager->call($plugin_name, 'block', $argument);
#        (defined($result)) ? $result : $original;
#    }emg;
#}

# 段落
sub paragraph {
    my $ref = shift;
    # 行頭がインライン要素タグ
    ${$ref} =~ s{^(<(?:[ib]|em|strong)>.*)}{<p>$1</p>}mg;
    ${$ref} =~ s{^(<(?:a |span).*)}{<p>$1</p>}mg;
    # 行頭が"<"と改行以外
    ${$ref} =~ s{^([^<\n].*)}{<p>$1</p>}mg;
    ${$ref} =~ s{</p>\n<p>}{<!--break-->}g;
}

# 改行の後処理
sub line_break {
    my $ref = shift;
    ${$ref} =~ s{<!--break-->}{\n}g;
}

# 目次追加
sub insert_toc {
    my $ref = shift;
    my $toc = '';
    my $count = 0;
    while (${$ref} =~ s{^<h([234])>(?!<a name="i\d+"> </a>)(.+?)</h\1>$}{<h$1><a name="i$count"> </a>$2</h$1>}m) {
        $toc .= '-' x ($1 - 1);
        $toc .= qq{<a href="#i$count">} . remove_tag($2) . qq{</a>\n};
        $count++;
    }
    &unordered_list(\$toc);
    $toc =~ s/<ul>/<ul class="toc">/;
    ${$ref} = $toc . ${$ref};
}

## 以下はオリジナルYukiWikiにあるサブルーチン(の代替)

# タグ削除
sub remove_tag {
    my ($line) = @_;
    $line =~ s{</?[A-Za-z][^>]*>}{}g;
    return $line;
}

# インライン要素の変換
sub inline {
    my $arg = shift;
    $arg = &escape($arg);
    $arg =~ s{'''([^']+?)'''}{<em><i>$1</i></em>}g;        # Italic
    $arg =~ s{''([^']+?)''}{<strong><b>$1</b></strong>}g;  # Bold
    return $arg;
}

# 実体参照に変換
sub escape {
    my $arg = shift;
    $arg =~ s/&/&amp;/g;
    $arg =~ s/</&lt;/g;
    $arg =~ s/>/&gt;/g;
    $arg =~ s/"/&quot;/g;
    return $arg;
}