PukiWikiプラグイン開発チュートリアル

Top > PluginDev > Tutorial
First Edition. 06/10/2007
Last Modified. 08/01/2008
Tag: PukiWiki PluginDev

PukiWikiプラグイン開発チュートリアル

Table of Contents

基本

プラグインは plugin ディレクトリに「<プラグイン名>.inc.php」として作成します。 内容は以下のような形式になります。

参照:dev:プラグイン/開発者向け dev:PukiWiki/Plug-inの仕様 dev:PukiWiki/1.4/InlinePlugin

<?php
// define() など。

function plugin_<プラグイン名>_init()
{ 
// 初期化関数。プラグインが呼び出されるとまず最初に実行されます。
// 成功 (true) か失敗 (false) を返す、もしくは何も返しません。
// 注:一度のセッション(PukiWiki実行)で一度だけ実行されるのが仕様です。
}
function plugin_<プラグイン名>_convert()
{
// ブロック型。行頭で #<プラグイン名>() と呼び出します。文字列を返します。
}
function plugin_<プラグイン名>inline()
{
// インライン型。行内で &<プラグイン名>(){}; と呼び出します。文字列を返します。
}
function plugin_<プラグイン名>_action()
{
// アクション型。index.php?cmd=<プラグイン名> でアクセスしたときに呼び出されます
// array('msg'=>'タイトル文字列','body'=>'本文'); を返します。本文が空文字の場合には read プラグインに移行します。
}
?>

初級

まずはやってみよう - Amazonおまかせ広告

まずは、ブロック型、インライン型の区別などは気にせずに、とにかく簡単なものを作ってみましょう。

Amazon おまかせ広告プラグインを作成してみようと思います。 動作的には単純に記述した html を返すだけです。 例えば Google Adsense やもちろん、 Amazon おまかせ広告などの Web API をとりあえず PukiWiki 上に表示したい場合にこのように作成します。

amazonads.inc.php というファイルを作成し、下記のコードを保存し、plugin ディレクトリにおきます。Wiki ページで

#amazonads

と行頭に記述することでこのプラグインを起動できます。

コード

<?php
function plugin_amazonads_convert()
{
    return '<div class="amazonads">
<script type="text/javascript">
<!-- <![CDATA[
amazon_ad_tag = "technique-22";
amazon_ad_width = "160";
amazon_ad_height = "600";
amazon_color_link = "000080";
amazon_ad_border = "hide";
//]]>-->
</script>
<script type="text/javascript" src="http://www.assoc-amazon.jp/s/ads.js"></script>
</div>';
}
?>

解説

  • <プラグイン名>.inc.php というファイル名で plugin ディレクトリに保存し、ファイル中で plugin_<プラグイン名>_convert という関数を定義するとブロック型プラグインを作れます。
  • ブロック型プラグインは、wiki ページの行頭で #<プラグイン名> と記述することで呼び出せます。
  • 動作的には文字列(HTMLタグ)を返しているだけです。

実行

#amazonads

定数を設定しよう - Amazonおまかせ広告発展

さきほどのコードでは Amazon広告ID として私の ID (technique-22) を該当箇所に直接記述(ハードコーディング)していました。 ユーザは各々自分の ID に変更したいはずなので、定数としてファイル先頭で定義し、 ユーザが変更箇所を楽に見極められるようにしておいてあげると親切です。

コード

<?php
defined('PLUGIN_AMAZONADS_ID') or define('PLUGIN_AMAZONADS_ID', 'technique-22');

function plugin_amazonads_convert()
{
    return '<div class="amazonads">
<script type="text/javascript">
<!-- <![CDATA[
amazon_ad_tag = "' . PLUGIN_AMAZONADS_ID . '";
amazon_ad_width = "160";
amazon_ad_height = "600";
amazon_color_link = "000080";
amazon_ad_border = "hide";
//]]>-->
</script>
<script type="text/javascript" src="http://www.assoc-amazon.jp/s/ads.js"></script>
</div>';
}
?>

解説

  • 先頭の define でユーザ定数を設定しています。defined は今後のためです。 定数の場合にも他のプラグインと名前がかぶらないように PLUGIN_<プラグイン名>_ のように接頭辞を付けておきます。 定数の名前はすべて大文字にしておくのが通例です。
  • 今回のように、サイト管理者がプラグイン設置時に一度設定したら、それ以降はもう変更しないような値を、定数として定義しておくとよいでしょう。 プラグイン利用者が、プラグイン実行毎に変更したいような値は、次の解説であるプラグインの引数として処理すると良いでしょう。
  • global 変数が必要になった場合も、$plugin_<プラグイン名>_ のように接頭辞をつけましょう。

実行

#amazonads

結果:略

引数を受け取ろう - フラッシュプラグイン

ユーザーがプラグイン呼び出しの際に引数を与えて、動作を変更できるようにします。 例としてフラッシュを表示するプラグインを作成してみましょう。

仕様

書式

#flash(URL,[オプション])

オプション

  • width=数字
  • height=数字
    • 高さ

コード

flash.inc.php として作成し、plugin ディレクトリに保存します。

<?php
function plugin_flash_convert()
{
    $args = func_get_args();
    if (count($args) < 1) { return '<div>flash(): #flash(URL,[option])</div>'; }
    $url = array_shift($args);
    $options = array('width'=>'', 'height'=>'');
    foreach ($args as $arg) {
        list($key, $val) = explode('=', $arg, 2);
        $options[$key] = $val;
    }
    $url = htmlspecialchars($url);
    foreach ($options as $key => $val) {
        $options[$key] = htmlspecialchars($val);
    }
    return <<<HTML
<div class="flash">
<object data="$url" width="{$options['width']}" height="{$options['height']}" type="application/x-shockwave-flash">
<param name="movie" value="$url" />
</object>
</div>
HTML;
}
?>

解説

  • プラグインの引数は func_get_args() 関数で配列として受け取れます。プラグイン呼び出しの際の , (カンマ)が引数の区切り文字です。
  • URL引数は必須のはずなので、引数が1つもない場合はエラーメッセージを返して終了します。if (count($args) < 1)
  • 先頭要素が URL のはずなので array_shift($args) で抜き出します。
  • 残りの引数は explode('=', $arg, 2); で width=159 のような文字列を 'width' と '159' に分割し、オプションとして値を保存していきます。
  • 受け取った文字を html 上に表示する際は htmlspecialchars 関数を用いてサニタイズを行い、つまり html タグを使用できないようにしましょう。これを怠ると wiki ユーザが自由に html を記述でき、javascript コードを埋め込んで他ユーザのブラウザクッキーを盗めるなど、いわゆる XSS 脆弱性が生じます。
  • 関数の詳細を見たい場合は上記コードでリンクが貼ってあるのでクリックしてみてください。

実行

#flash(http://www.adobe.com/swf/software/flash/about/ja/flashAbout_small.swf,width=159,height=91)

Your browser is not supporting object tag. Please use one of the latest browsers.

インライン型 - 相対リンクプラグイン

インライン型プラグインは行内で

&プラグイン名(引数リスト){文字列}; 

のように呼び出します。ブロック型と異なり、行頭以外でも呼び出せます。

ならばすべてインライン型で良いのではないかと思うかもしれませんが、そうではありません。そこは中級で説明します。 今はとりあえず簡単なインラインプラグインを作成してみましょう。


PukiWiki の URL から相対アドレスでリンクを貼れるプラグインを作成してみます。 pukiwiki から同サイト上のファイルにリンクを貼る際に http から始める必要がなくなります。

仕様

&linkr(相対アドレス){リンク文字列};
  • リンク文字列は省略時、相対アドレスで指定された文字を使用する

コード

linkr.inc.php として作成し、plugin ディレクトリに保存します。

<?php
function plugin_linkr_inline()
{
    $args = func_get_args();
    if (count($args) < 2) { return 'linkr(): &amp;linkr(url){linkstr};'; }
    $linkstr = array_pop($args); // drop {}
    $href    = array_shift($args);
    $href    = htmlspecialchars($href);
    $linkstr = ($linkstr !== '') ? $linkstr : $href;
    return '<a class="linkr" href="' . $href . '">' . $linkstr . '</a>';
}
?>

解説

  • {} 引数の内容は常に引数配列 $args の一番最後の要素に格納されます。
  • {} が省略された場合、空文字が指定されたのと同じです。つまり、引数配列の一番最後の要素に空文字が格納されます。
  • よって {} を使用しないプラグインの場合、array_pop($args); // drop {} として最後の要素はとりあえず捨ててしまうのが通例です。
  • {} の文字列はサニタイズ済み(正確には pukiwiki の make_link という関数が呼ばれます)なので、htmlspecialchars を使用する必要はありません。

実行

&linkr(phpdoc){PukiWiki PHPDoc};

PukiWiki PHPDoc

マルチラインブロック型 - pre プラグイン

pukiwiki 1.4.6 からブロック型プラグインにマルチライン引数がサポートされ、幅が広がりました。この機能を使うにはユーザに pukiwiki.ini.php で

define('PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK', 0); // 1 = Disabled

と設定して有効にしてもらいます。マルチラインプラグインは

#plugin(){{
}}

のように起動されます。入れ子は

#plugin2(){{{
#plugin1(){{
}}
}}}

のように {} の数を増やことで可能です。


この機能を利用して pre プラグインを作成してみます。pre 要素を使用するために各行の先頭にちくちく空白文字を挿入せずにすむようになります。

仕様

#pre{{
文章
}}

コード

pre.inc.php として作成し、plugin ディレクトリに保存します。

<?php
function plugin_pre_convert()
{
    $args = func_get_args();
    $end = end($args);
    if (substr($end, -1) == "\r") {
        return '<p>pre(): no string.</p>';
    }
    return '<pre class="pre">' . htmlspecialchars($end) . '</pre>';
}
?>

解説

  • マルチライン引数の内容は引数配列の最後に格納されます。
  • ただし、インライン型の場合とは違い、省略された場合、引数配列には何も格納されません
  • 引数配列の最後の要素が、マルチライン引数のものかどうかの判定は文字列の最後に "\r" 改行文字列があるかどうかで判定できます substr($end, -1) == "\r"
  • マルチライン引数内の改行文字はすべて wiki ページ内の通常の改行文字 "\n" から "\r" に変換されています。

実行

#pre{{
<b>test</b>
}}
<b>test</b>

アクション型 - grep プラグイン

アクション型プラグインはブラウザから index.php?cmd=プラグイン名 のようなアドレスにアクセスしたり(GET)、 フォームから投稿(POST)する際に起動します。


ページ内の文字列を検索する grep プラグインを作ってみます。

仕様

  • フォームなし。URL で直接引数値を指定する(GET)
  • index.php?cmd=grep&page=ページ名&grep=検索文字列(正規表現)

wiki ソース内で正規表現にマッチした行を抜きだし、行番号と共に <pre></pre> で表示する。

コード

grep.inc.php として作成し、plugin ディレクトリに保存します。

<?php
function plugin_grep_action()
{
    global $vars, $defaultpage;
    $page = isset($vars['page']) ? $vars['page'] : $defultpage;
    if (! is_page($page)) {
        $body = '<p>' . htmlspecialchars($page) . ' does not exist.</p>';
        return array('msg'=>'Grep Plugin', 'body'=>$body);
    }        
    if (! check_readable($page)) {
        $body = '<p>' . htmlspecialchars($page) . ' is not readable.</p>';
        return array('msg'=>'Grep Plugin', 'body'=>$body);
    }
    $grep = isset($vars['grep']) ? $vars['grep'] : '';
    $lines = get_source($page);
    $lines = preg_grep('/' . preg_quote($grep, '/') . '/', $lines);
    $contents = '';
    foreach ($lines as $i => $line) {
        $contents .= sprintf('%04d:', $i) . htmlspecialchars($line);
    }
    $body = '<pre>' . htmlspecialchars($contents) . '</pre>';
    return array('msg'=>'Grep Plugin', 'body'=>$body);
}
?>

解説

  • グローバル変数 $vars で引数の値を受け取れます(PHP)。これは通常の PHP アプリケーションと同じです。
  • グローバル変数 $defaultpage には、pukiwiki.ini.php で設定したデフォルトページ名(通常は FrontPage) が格納されています。
  • is_page 関数で、その文字列が wiki ページ名として正しいか、そしてそのページが存在しているかがチェックできます。
  • check_readable 関数で、その wiki ページを読み込むことができるか(読み取り制限されていないか)のチェックができます。読み取り制限されている場合 Basic 認証ウィンドウが表示されます。
  • get_source 関数で、wiki ページのソースコードを配列として受け取れます。1行が1要素に保存されます。
  • preg_grep 関数で正規表現にマッチした行だけを抜き出します(PHP)。
  • アクション型の返り値は array('msg'=>$msg, 'body'=>$body); のような形をとります。msg で指定された文字列は HTML ページタイトルとして表示されます。body で指定された文字列は本文になります。

実行

cmd=grep&page=PluginDev%2FTutorial&grep=inc.php

アクション型(フォーム) - grepall プラグイン

先の grep プラグインを改良して、フォームを表示して、ユーザが値をフォームに入力できるようにします。 さらに1ページだけの検索ではなく、複数ページの検索をできるようにします。 コードが初級にしては長くなってしまった気もしますが、アクション型はこれぐらいはしないと正直使い物になりません。

仕様

フォームを表示。フォームは「ページ名」、「ページ名絞込み(正規表現)」、「検索文字列(正規表現)」の3つ

  • ページ名が指定された場合そのページを grep。
  • ページ名絞込みが指定された場合全ページからその正規表現にマッチするページだけに限定して grep。
  • どちらも指定されなかった場合、全ページを grep

コード

grepall.inc.php として作成し、plugin ディレクトリに保存します。

<?php
function plugin_grepall_action()
{
    global $vars;
    if (isset($vars['pcmd']) && $vars['pcmd'] == 'grep') {
        $body = plugin_grepall_grep();
    } else {
        $body = plugin_grepall_get_form();
    }
    return array('msg'=>'Grep All Plugin', 'body'=>$body);
}

function plugin_grepall_grep()
{
    global $vars, $defaultpage;
    $page   = isset($vars['page']) ? $vars['page'] : $defultpage;
    $filter = isset($vars['filter']) ? $vars['filter'] : '';
    $grep   = isset($vars['grep']) ? $vars['grep'] : '';

    // page lists
    if ($page !== '') {
        if (! is_page($page)) {
            $body = '<p>' . htmlspecialchars($page) . ' does not exist.</p>';
            return $body;
        }        
        $pages = (array)$page;
    } else {
        $pages = get_existpages();
        if ($filter !== '') {
            $pages = preg_grep('/' . preg_quote($filter, '/') . '/', $pages);
        }
    }
    // grep
    $body = '';
    foreach ($pages as $page) {
        if (! check_readable($page)) {
            $body = '<p>' . htmlspecialchars($page) . ' is not readable.</p>';
            return $body;
        }
        $lines = get_source($page);
        $lines = preg_grep('/' . preg_quote($grep, '/') . '/', $lines);
        if (empty($lines)) continue;
        $contents = '';
        foreach ($lines as $i => $line) {
            $contents .= sprintf('%04d:', $i) . htmlspecialchars($line);
        }
        $body .= make_pagelink($page) . '<br />' . "\n";
        $body .= '<pre>' . htmlspecialchars($contents) . '</pre>';
    }
    return $body;
}

function plugin_grepall_get_form($msg = "")
{
    $form = '';
    $form .= '<form action="' . get_script_uri() . '?cmd=grepall " method="post">' . "\n";
    $form .= '<div>' . "\n";
    $form .= ' <input type="hidden" name="pcmd"  value="grep" />' . "\n";
    $form .= ' <input type="text" name="page" size="24" value="" /> Page Name<br />' . "\n";
    $form .= ' <input type="text" name="filter" size="24" value="" /> Filter Pages (Regular Expression)<br />' . "\n";
    $form .= ' <input type="text" name="grep" size="24" value="" /> Search String (Regular Expression)<br />' . "\n";
    $form .= ' <input type="submit" name="submit" value="Grep All!" /><br />' . "\n";
    $form .= '</div>' . "\n";
    $form .= '</form>' . "\n";
    if ($msg != '') {
        $msg = '<p><strong>' . $msg . '</strong></p>';
    }
    return $msg . $form;
}

?>

解説

  • パラメータ $vars['pcmd'] によってフォーム表示か、検索処理かの動作を選びます。
  • プラグイン内で関数を定義する場合は、他のプラグインの邪魔にならないように plugin_プラグイン名_関数名 のように命名します。
  • plugin_grepall_get_form がフォーム表示の関数です。
    • get_script_uri() で pukiwiki のアドレスを取得できます。
    • cmd パラメータで POST 時起動されるプラグインを指定します。今回は自分 (grepall) を起動しています。
    • pcmd パラメータで $vars['pcmd'] に渡る値を指定しています。
    • pcmd パラメータ含め、そのほかは通常の PHP アプリケーションと変わりありません。アクション型は pukiwiki の関数を使用できる単独の PHP アプリケーションと考えることもできます。
  • plugin_grepall_grep が検索処理の関数です。
    • get_existpages() 関数で存在している wiki ページ名全てを取得できます。
    • preg_grep を用いて、パラメータ filter の値でページ群を限定しています。
    • 後は前回とほぼ同じですが、
    • make_pagelink でページ名へのリンクを作成して、出力に付加しています。

実行

cmd=grepall

初期化関数

チュートリアルを通して初期化関数 plugin_<プラグイン名>_init を使用した例を書くのを忘れていたようなので追記しておきます。

例えば、ブロック型プラグインと、アクション型プラグインで同じオプションを扱う以下のような場合があったとします。

<?php
function plugin_xxx_convert()
{
    $options = array( // オプション型宣言
        'num' => null,
        'depth' => null,
    );
    $args = func_get_args();
    $argoptions = array();
    foreach ($args as $arg) { // key=val 分離だけ
        list($key, $val) = array_pad(explode('=', $arg, 2), 2, true);
        $argoptions[$key] = $val;
    }
    foreach ($argoptions as $key => $val) { // 型チェック
        if (array_key_exists($key, $options)) {
            $options[$key] = $val;
        }
    }
}
function plugin_xxx_action()
{
    $options = array( // オプション型宣言
        'num' => null,
        'depth' => null,
    );
    $argoptions = $vars; // 分離は終了している
    foreach ($argoptions as $key => $val) { // 型チェック
        if (array_key_exists($key, $options)) {
            $options[$key] = $val;
        }
    }
}

上記のように書くと、$options の型定義を両方で書いているので修正時に手間です。 そこで、以下のようにすれば、一箇所だけ修正すればすむようになります。

<?php
function plugin_xxx_init()
{
    global $plugin_xxx_options;
    $plugin_xxx_options = array( // オプション型宣言
        'num' => null,
        'depth' => null,
    );
}
function plugin_xxx_convert()
{
    global $plugin_xxx_options;
    $args = func_get_args();
    $argoptions = array();
    foreach ($args as $arg) { // key=val 分離だけ
        list($key, $val) = array_pad(explode('=', $arg, 2), 2, true);
        $argoptions[$key] = $val;
    }
    $options = $plugin_xxx_options;
    foreach ($argoptions as $key => $val) { // 型チェック
        if (array_key_exists($key, $options)) {
            $options[$key] = $val;
        }
    }
}
function plugin_xxx_action()
{
    global $plugin_xxx_options;
    $argoptions = $vars; // 分離は終了している
    $options = $plugin_xxx_options;
    foreach ($argoptions as $key => $val) { // 型チェック
        if (array_key_exists($key, $options)) {
            $options[$key] = $val;
        }
    }
}

ここで、初期化関数 plugin_<プラグイン名>_init は一度のセッション(PukiWiki実行)で一度だけ実行されることを思い出してください(正確には、プラグイン初回呼び出し時のみ、プラグイン実行前に実行されます)。なので、プラグインが実行される毎に、$plugin_xxx_options への同じ代入が行われることなく、効率的です。

以下、独り言。

以上が、無理に初期化関数 plugin_<プラグイン名>_init を使用する例です。

convert, action 関数内で結局、

$options = $plugin_xxx_options;

と代入し直しているので正直効率的でもなんでもないです。 それに、init 関数での結果を受け取るにはグローバル変数にしないといけない(グローバル空間を汚さなければならない)というのもいただけません。 以下のように記述し直せば、グローバル空間を汚すこともなくなります。

このように、初期化関数は自力でどうとでもできる場合が多いです。プラグインを自作する際は忘れてもらっても良いでしょう(他人のコードを読む場合は知識が必要になりますが)。 こういう理由で、チュートリアルを通して初期化関数 plugin_<プラグイン名>_init を使用した例が存在しなかったわけです。

function plugin_xxx_get_options()
{
    static $options = array( // オプション型宣言
        'num' => null,
        'depth' => null,
    );
    return $options;
}
function plugin_xxx_convert()
{
    $args = func_get_args();
    $argoptions = array();
    foreach ($args as $arg) { // key=val 分離だけ
        list($key, $val) = array_pad(explode('=', $arg, 2), 2, true);
        $argoptions[$key] = $val;
    }
    $options = plugin_xxx_get_options();
    foreach ($argoptions as $key => $val) { // 型チェック
        if (array_key_exists($key, $options)) {
            $options[$key] = $val;
        }
    }
}
function plugin_xxx_action()
{
    $argoptions = $vars; // 分離は終了している
    $options = plugin_xxx_get_options();
    foreach ($argoptions as $key => $val) { // 型チェック
        if (array_key_exists($key, $options)) {
            $options[$key] = $val;
        }
    }
}

中級

中級では、自分用にだけプラグインを制作していたレベルから、プラグインを配布するレベルに上がることを目的とします。

ブロック?インライン?

ブロック型プラグイン、インライン型プラグインという名称が出てきますが、ブロック、インラインというものになじみがない人が多いかもしれません。 これは HTML 言語による分類で、プラグインが出力する HTML に応じてどちらの型のプラグインにすべきかが決定します。 プラグイン開発においては、基礎のようなものなのですが、きちんと把握するにはそれなりの知識が必要になります。

「HTML ブロック インライン」で検索すると多くのサイトがでてきますが、とりあえずは ブロック要素とインライン要素の違い のサイトを参照するのが良さそうに見えます。勉強してください。HTML 標準である W3C へのリンクも一応貼っておきます(厳密な定義なのでわかりにくいかもしれません)。

例えば HTML

<div>hoge</div>

はブロック要素になります。タグを使用しない

hoge

<span>hoge</span>

はインライン要素になります。

勉強したとおり、HTML のインライン要素は「行内」で用いることができ、ブロック要素はインライン要素に含まれることができません(「行内」で使用できない)。これがプラグインのそれぞれの型の呼び出し方の制限の理由になります。

つまり、インライン要素は行内で用いることができるためインライン型プラグインも行内で用いることができ、

文章 &hoge; 文章

のように呼び出せます。ブロック要素は行内で使用できないためブロック型プラグインも行内で用いることができず、必ず

#hoge

のように行頭で呼び出すことになります(正確にはテーブルセルの先頭でも呼び出せます)。

ちなみに対応する実装は

<?php
function plugin_hoge_convert()
{
    return '<div>hoge</div>';
}
function plugin_hoge_inline()
{
    return '<span>hoge</span>';
}
?>

のようになります。

インライン型のほうが柔軟なわけですので、インライン型で充分ならばブロック型は実装する必要はないでしょう。 ですが、color プラグインや size プラグインのような単純なプラグイン以外は結局、ブロック型になることが多いです。 PukiWiki 1.4.6 以上ではマルチライン引数を受け付けることもできて便利ですし。

XHTML1.1+便利ソフトウェア

さきほどの ブロック?インライン? のように、実は PukiWiki のプラグイン開発はかなりの (X)HTML の知識を必要とします。「表示できればそれでよい」のレベルから脱却する必要があります。

PukiWiki は HTML の DOCTYPE として XHTML 1.1 を採用しています。XHTML 1.1 はかなり厳しい標準で、例えば

  • a 要素で target 属性を使用してはならない*1
  • iframe タグを使用してはならない(→ object タグに統一)
  • embed タグを使用してはならない(→ object タグに統一)

などなどいろいろ制限があります。正しい XHTML の書き方を勉強する必要があります。(しかし、実際の所全てのブラウザが XHTML 1.1 に準拠できているかというとそういうわけでもなく、例えば IE では object タグがまともに動作しないため、さらなる工夫が必要になったりします。)

ここでは正しい XHTML 1.1 であることをチェックするツールである Validator の紹介をしておきます。 チェックしては修正していくうちに学んでいけると思います。

同じ HTML, CSS でもブラウザによって表示が異なる場合があります。複雑なHTMLやCSSを使用していると思ったら、それぞれのブラウザでチェックしておきましょう。Firefox, Opera, IE6、IE7、さらには Safari (MacOSX) をサポートできれば充分かと思います。

  • Firefox
  • Opera
  • Standalone IE 3-6 IE7とは別にIE6をインストールしておけます
  • IE 5.2.3 for Mac Mac は IE をもはや正式対応していないので私はこれでチェックはしてません
  • IE7 は Windows で。
  • Safari は MacOSX で。追記:safari 今日 2007/06/11、Windows 対応 Safari が公開されました。

さらに Firefox 用ですが、私が2007年6月現在使用しているウェブ開発用の拡張を紹介しておきます。

  • Web Developer 老舗中の老舗です。
  • Fire Bug これは比較的新しい拡張で、便利としか言いようがないほどに便利です。HTML の要素構造を調べたり、javascript のエラー箇所を参照したり、CSS をリアルタイムで変更したりできます(Web Developer にも CSS をリアルタイムで変更する機能がありますがより使いやすくなっています)。
  • HTML Validator ソースを開くと HTML Validate の結果がついてきます。前は Web Developer の Tools > Validate HTML の機能を使用していたのですが、その必要がなくなりました。localhost の html も楽にチェックできます。
  • IEView もしくは IETab 現在開いているページを IE で開きます。個人的には IEView のほうを使用しています。

フラッシュプラグイン完全版

初級の flash.inc.php を発展させてみます。初級ではただのコピーで XHTML タグを意識しなかったと思いますが、中級では XHTML1.1 準拠の XHTML タグの用意から意識して作成してみましょう。

まず、フラッシュを表示するにはどのタグをどのように使用するのか、またどのようなパラメータがありえるのかを調査します。ウェブ検索を行い、次のページに辿り付きました。

このサイトで使用している HTML は XHTML ではなく HTML4.01 でしょうか。ただのコピーでは使用できません。embed タグは XHTML1.1 では使用できないこともあり、XHTML1.1 で Flash を表示する方法を探します。

これで XHTML タグの準備はできました。やっとプラグインを書けます。プラグイン本体を書くよりも明らかに調査の時間のほうがかかっています。

仕様

書式

#flash(URL,[option ...])

オプション

  • width
  • height
    • 高さ
  • flashvars
    • フラッシュに渡す引数
  • quality=low|medium|high|best|autolow|autohigh
    • 画質。デフォルトは high
    • 「low」[低]アンチエイリアスされません。ビットマップはスムージングされません。
    • 「medium」[中]若干アンチエイリアスされます。ビットマップはスムージングされません。
    • 「high」[高]全てアンチエイリアスされます。ただしアニメーションを含むビットマップはスムージングされません。
    • 「best」[品質優先](最高画質)全てアンチエイリアスされます。全てのビットマップはスムージングされます。
    • 「autolow」[自動/低]低画質でスタートしプロセッサの処理速度に合わせて画質が自動調整されます。
    • 「autohigh」[自動/高]高画質でスタートしプロセッサの処理速度に合わせて画質が自動調整されます。
  • bgcolor
    • 背景色。
  • loop=true|false
    • ループするか否か。デフォルトは true
  • play=true|false
    • ロードされたのと同時に再生する否か。デフォルトは true
  • scale=showall|noborder|exactfit|noscale
    • 表示スケール。デフォルトは showall
    • 「showall」[すべて表示]縦横比を維持し指定サイズ内にムービー全体を表示します。
    • 「noborder」[枠なし]縦横比を維持し指定サイズいっぱいにムービー全体を表示します。
    • 「exactfit」[フィット]ムービーの縦横比が変化し指定サイズにぴったり収まるように表示されます。
    • 「noscale」[拡大/縮小なし]Flashムービー内で指定されているサイズで表示されます。伸縮されません。
  • salign=L|R|T|B|TL|TR|BL|BR
    • 表示領域のどこに揃えて配置するか指定
    • 「L」[左]「R」[右]「T」[上]「B」[下]「TL」[左上]「TR」[右上]「BL」[左下]「BR」[右下]
  • base=URL
    • ムービー内の相対パス指定の基準になるディレクトリやURL
  • menu=true|false
    • 右クリック時に表示されるメニューについて設定します。
    • 「true」で全てのメニューが表示されます。「false」でいくつかのメニューを省略して表示します。
  • wmode=window|opaque|transparent
    • Windows版のInternet Explorer 4.0以上で機能します。
    • 「window」[標準]ページ上で専用の四角形の窓でムービーが表示されます。
    • 「opaque」[不透明表示]ムービーの背後にあるものはすべて表示されなくなります。
    • 「transparent」[透明表示]ページの背景がムービーの透明な部分を通して透けて見えるようになります。
  • allowScriptAcess=sameDomain|never|always Third-party storage
    • Third-party クッキーの許可範囲。デフォルトは sameDomain
  • id
    • フラッシュが内部で使用するかもしれないID
  • style
    • XHTML コードに付加する CSS style

コード

デフォルト値はプラグイン側で設定せずに、 flash player にまかせることにします。

ダウンロード svn:plugin/flash.inc.php

<?php
function plugin_flash_convert()
{
    $args = func_get_args();
    if (count($args) < 1) {
        return '<div>flash(): #flash(URL,[option])<div>';
    }
    $url = htmlspecialchars(array_shift($args));
    $options = array(
         'width' => '',
         'height' => '',
         'flashvars' => '',
         'quality' => '',
         'bgcolor' => '',
         'loop' => '',
         'play' => '',
         'scale' => '',
         'salign' => '',
         'base' => '',
         'menu' => '',
         'wmode' => '',
         'allowScriptAccess' => '',
         'style' => '',
         'id' => '',
    );
    foreach ($args as $arg) {
        list($key, $val) = array_pad(explode('=', $arg, 2), 2, TRUE);
        if (isset($options[$key])) {
            $options[$key] = $val;
        }
    }
    $options['flashvars'] = rawurlencode($options['flashvars']);
    foreach ($options as $key => $val) {
        $options[$key] = htmlspecialchars($val);
    }

    return <<<HTML
<div class="flash" style="{$options['style']}">
<object data="{$url}" width="{$options['width']}" height="{$options['height']}" id="{$options['id']}" type="application/x-shockwave-flash">
<param name="movie" value="{$url}" />
<param name="FlashVars" value="{$options['flashvars']}" />
<param name="quality" value="{$options['quality']}" />
<param name="bgcolor" value="{$options['bgcolor']}" />
<param name="loop" value="{$options['loop']}" />
<param name="play" value="{$options['play']}" />
<param name="scale" value="{$options['scale']}" />
<param name="salign" value="{$options['salign']}" />
<param name="base" value="{$options['base']}" />
<param name="menu" value="{$options['menu']}" />
<param name="wmode" value="{$options['wmode']}" />
<param name="allowScriptAccess" value="{$options['allowScriptAccess']}" />
<p>Your browser is not supporting object tag. Please use one of the latest browsers.</p>
</object>
</div>
HTML;
}
?>

実行

#flash(http://www.adobe.com/swf/software/flash/about/ja/flashAbout_small.swf,width=159,height=91)

Your browser is not supporting object tag. Please use one of the latest browsers.

コーディング規約+フォーマッタ

PukiWiki のコーディング規約は

に準拠しています。PEAR標準コーディング規約dev:BugTrack/779 クリンナップのまとめ、を参照しておいてください。

ここでは少しだけ pukiwiki 標準の解説をしておきます。

文字コード

  • 改行文字は Windows 標準 "\r\n" ではなく UNIX 標準 "\n" です。
  • 文字コードは PukiWiki が EUC-JP 版なら EUC-JP ですし、UTF-8 版なら UTF-8 です。それらの文字コードを扱えるエディタを使用する必要があります。

文字列

いくつかのサンプルでは簡単のためヒアドキュメントを使用していましたが、 ヒアドキュメントを正しく処理できるフォーマッタが実際の所ありません。

$html = <<<HTML
<b>$hoge</b>
HTML;

とせずに

$html = '';
$html .= '<b>' . $hoge . '</b>' . "\n";

とするのが良いでしょう。dev:BugTrack/779 クリンナップのまとめにあるように " (ダブルクォーテーション)もあまり使用しないようにします。

関数名

プラグイン中で関数を定義する場合は、他のプラグインと名前が被らないように

function plugin_プラグイン名_関数名
{
}

とします。必然的に長くなってしまい、関数を使用しにくいのですが仕方ありません。 英数小文字を使用します。

ちなみに、関数、クラスの場合の { は次の行から始まりますが、他の if や while, foreach などの場合は

if (true) {
} elseif (false) {
} else {
}

のように同行に書きます。これは PEAR 標準です。

定数名

定数も同様に、他のプラグインと名前がかぶらないようにします。

defined('PLUGIN_プラグイン名_定数名') or define('PLUGIN_プラグイン名_定数名', 値);

のようにします。英数大文字を使用します。defined() or は今後のためです。 定数は一度しか定義できないので、どこかよそですでに定義されていた場合、warning が出ます。それを防ぎます。

cssクラス

各プラグインで返す html に css クラスを設定しておいてあげるとユーザが各自、プラグインを改変せずに見た目を変更することができるようになるので親切です。

function plugin_hoge_convert()
{
    return '<div class="hoge">hoge</div>';
}
function plugin_hoge_inline()
{
    return '<span class="hoge">hoge</span>';
}

整形ツール

整形ツールを用いて整形していくうちに徐々に学べると思います。 ただ、あまり使用したことがないので、どれが良いのかわかりません。情報求む。

また、なんらかの PHP 対応エディタを使用すれば整形機能もついていると思います。 ただ、pukiwiki 標準に合わせるための設定は必要かと思います。

PHP 関数 と PukiWiki 関数

道中、少しずつ関数を説明してきましたが、一覧が欲しいはずです。 リンクを貼っておきます。

PHP の関数は php.net で調べるのがベストです。大体この本家サイトだけで事足ります。配列、文字列、ファイル関数はとりわけ良く使用するので、それらのページにも特別にリンクを貼っておきます。

PukiWiki の関数の説明は

がありますが、古いですし説明が足りません。pukiwiki のソースコード内検索をしないとわからないです。 私は find コマンドを用いて (cygwin on windows, linux)、

find lib/ | xargs grep '関数名'
find plugin/ | xargs grep '関数名'

のように検索し、定義や使用法を実際のコードから解読したりします。 せっかくのオープンソースなので他者のソースコードから学びましょう。

また、phpDocumentor を使用して phpdoc

を作成してみましたが、PukiWiki のソースコード自体が phpdoc コメントを記述していないので、関数の型が多少わかるぐらいのあまり役にたたない状態です。

参考になるコード

まず、デフォルトで PukiWiki に梱包されているプラグインが参考になります。 今まで運用してきて出会ったような、少し考えただけでは気付かないような状況にもきちんと対応しています。 また、もちろん PukiWiki 標準に即しているので真似していけば、いずれ PukiWiki 標準のコードの書き方が身に付くでしょう。

独自

読まなくても良いです。

PukiWiki プラグイン開発において、あえてルールを破っている、私独自の手法をここで紹介します。拙作のプラグインのコードを読むときの参考にしてください。 こんな私もパッチを投げるときはもちろん PukiWiki 標準に従っています。あくまでも拙作プラグインにおいてだけです。

インデントは空白4文字

PukiWiki 標準としては、インデントはタブ文字ですが、私は PEAR 標準に合わせて空白4文字にしています。

理由は、PEAR が言うように、タブ文字はエディタの設定により表示が異なるため(空白2文字相当に設定したり、4文字相当に設定したり)、表示が統一されないためです。 特にブラウザで見ると、空白8文字相当になってしまうのが痛いです。広すぎます。

クラス化

プラグインがある程度複雑化してきた場合、関数等を全てクラスの中にいれています。

全ての関数に plugin_<プラグイン名>_ 接頭辞を付けるのが煩わしいので、

class Plugin<プラグイン名> {
    function convert()
    {
    }
}

のように、クラスを作成し、その中に関数を書くことで、接頭辞をつけずにすむようにします。 また、設定定数も PLUGIN_<プラグイン名>_ を付けるのが煩わしいのでクラスの中の static 変数 $conf に格納しています。

どのように実現しているのかは、クラス化/クラス変数版 を、 そこにたどり着くまでの議論を読みたい方は クラス化 のページを参照してください。

クラス内関数は static

クラスを作成しましたが、(Plugin クラスの場合は特に)関数はクラス関数(static 関数)として利用できるように気をつけています。再利用性が高まります(PHP4 でも利用できるように static 修飾子はつけません。$this-> のない関数です)。

class Plugin<プラグイン名> {
    function file_put_contents($filename, $data)
    {
    }
}

static 関数ならば、他の場所にコピーしただけで再利用可能になりますし、それ故、単独テストもやりやすいです。

class Plugin<プラグイン名> {
    var $filename;
    var $data;
    function file_put_contents()
    {
        $filename = $this->filename;
        $data = $this->data;
    }
}

だとそうはいきませんよね。

結果として、class はメンバ変数を持たず、ただの名前空間として作用しているプラグインが多いです。 従来方式の関数をただ、class にいれて、接頭辞を除いただけ、なんてこともあります。

関数を、plugin_<プラグイン名>_<関数名> と呼ぶのと、Plugin<プラグイン名>::<関数名> と呼ぶのは、 たいして煩わしさは変わっていないような気もしますが、やはり再利用性を考えると後者が勝ります。 前者の場合は、どこか他所にコピペする場合に plugin_<プラグイン名>_ の部分を消す手間が出てきますよね。

一番再利用しやすいのは、もちろんきちんとライブラリの形に整えることですが、それの前段階のようなものです。

defined() or define()

私も簡単なプラグインの場合は、スタンダードなプラグイン製作の形で定数を使うことがあります。

その際、PukiWiki Plus! のユーザ設定機構に対応するため、define だけを書くのではなく、

-define('PLUGIN_<プラグイン名>_<変数名>', <値>);
+defined('PLUGIN_<プラグイン名>_<変数名>') or define('PLUGIN_<プラグイン名>_<変数名>', <値>);

のように defined() or も書くようにしています。 こうしておくことで、init/<プラグイン名>.ini.php で先回りして定数を設定しておいても、ワーニングがでないようになります。

PukiWiki Plus! の機能をできるだけ使用したプラグインの作成は、PluginDev/TutorialPlus を参照してください。

独自ライブラリ

以前は、ユーザの利便性を考えて、プラグインファイルを1つ落とせば単独として使える、というのを念頭においてやっていましたが、同じ関数をいくつものプラグインに書くことが多くなり、さすがに辛くなってきたので、独自ライブラリを作成することにしました。

http://svn.sourceforge.jp/svnroot/lsx/plugin/sonots/

phpdoc も作ってあります。

phpdoc/sonots

特に sonots.class.php 内の、PHP API 拡張、PukiWIki API 拡張関数は参考になるかもしれません。

独自ライブラリ名前規約

独自ライブラリ作成時に、勝手に名前規約を考えてみました。こうすることで名前がかぶらずにすむと思います。

名前規約 参照。


*1 どのウィンドウで開くかどうかはサイト管理者が強制することではなくユーザの操作でユーザが選ぶべきもの