はまったというか、別の意味で。
前置き長いです。

ここしばらくNucleusでのスキン制作環境について考えていました。
Nucleusはページレイアウトの記述(正確にはレイアウトや、個々のパーツ)が、

  • スキンやテンプレート
  • スキンディレクトリ内の外部ファイル
  • プラグインが独自に保持する表示テンプレート(プラグインオプションや独自の管理ページで設定)

に分散しているので、あちこち見て回る必要があって煩雑になりがち。

それに対する解決策がいくつか出ています。
外部ファイルを管理画面上で手軽に扱えるようにしたNP_SkinFiles(コアに同梱)。
外部ファイル化したコードのようないわゆる構成パーツをNP_ExtraSkinJPのようにDB上に持たせ、スキン画面で扱えるようにしたNP_includespecial
スキン、テンプレート、外部ファイルの各管理画面の遷移の煩雑さをスムーズに解消するNP_LinkToSkinFiles

残る課題が、プラグイン独自のテンプレート。
このうち一部はプラグイン独自の条件分岐が可能になるif拡張(doIf)によってスキンへ記述を移すことができるようになったけど、繰り返し内容を表示する場合に使われるようなテンプレートには向きません。

この部分の記述をスキン・テンプレに一緒に収めることができない(=読込/書出でまとめて扱えない)という点が不満だったので、MTライクなコンテナ・タグを実現するプラグインのテストコードを書いてみました。

/*これはテストコードです*/
class NP_Container extends NucleusPlugin {
    function getName() { return 'Container'; }
    function getAuthor()  { return 'yu'; }
    function getURL() { return 'http://nucleus.datoka.jp/'; }
    function getVersion() { return '0.1'; }
    function getMinNucleusVersion() { return 330; }

    function getDescription() { 
        return "test imprementation of container tag";
    }

    function supportsFeature($what) {
        switch($what){
            case 'SqlTablePrefix':
                return 1;
            default:
                return 0;
        }
    }

    function init() {
        $this->cnt = 0;
    }

    /* get container args and parse container (begin/end) */
    function doSkinVar($skinType) {
        $params = func_get_args();

        foreach ($params as $param) {
            list($type, $value) = explode(':', $param);
            switch ($type) {
            case 'mode':
                $this->mode = $value;
                break;
            case 'max':
                $this->max = (int)$value;
                break;
            case 'begin':
                ob_start();
                break;
            case 'end':
                $buff = ob_get_contents();
                ob_end_clean();

                $parts = $this->_get_parts($buff); //expects 'header', 'body', and 'footer'

                echo $parts['header'];
                while ($this->cnt++ < $this->max) $this->_parse($parts['body']);
                echo $parts['footer'];
                break;
            default:
            }
        }
    }

    /* parse var tag in container */
    function _parse($data) {
        echo preg_replace_callback("/<:([0-9a-zA-Z_-]+):>/", array(&$this, '_cb_parsevar'), $data);
    }

    function _cb_parsevar($m) {
        switch ($m[1]) {
        case 'echo':
            if ($this->mode) 
                return $this->mode . $this->cnt; //test output
            else 
                return 'brahbrah';
            break;
        default:
        }
    }

    /* get template parts */
    function _get_parts($buff) {
        $parts = array();
        $data = preg_split("{<part name=\"([a-z]+)\">|</part>}", $buff, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
        while ($data) {
            $type = array_shift($data);
            $type = trim($type);
            if ( empty($type) ) continue;
            $parts[$type] = array_shift($data);
        }
        return $parts;
    }
}

これで次のようなスキンの書き方ができる。

<%Container(begin,mode:local,max:3)%>
<part name="header">
<p>コンテナ内で他のスキン変数 <%if(category)%><%category(name)%><%else%>カテゴリ未選択<%endif%></p>
<dl>
<dt><%blogsetting(name)%></dt>
</part>
<part name="body">
<dd>ループ内の出力 <:echo:></dd>
</part>
<part name="footer">
</dl>
</part>
<%Container(end)%>

スキン上のコンテナ(beginend)内に、header, body, footer という3つの独自パーツを定義。プラグインはパーツを元に複数のbodyをループ処理して整形出力。上記の場合、headerfooterを独自パーツとして持つことはあまり意味がないけど、ここでは複数のパーツをまとめて記述、というのを見せる目的で。
表示コードをこのようにスキンに集中できればプラグインオプションがすっきりするし、スキン別にそれぞれ表示をカスタマイズできるメリットがある。

このコードでもったいないのは、ループ処理内の自身のスキン変数を<:name:>のような表記にして独自パースせざるを得ないこと。こうしないと初回にパースされた内容を無意味に繰り返し出力するだけのプラグインになってしまう(そういえばやっぱりここで大ハマリしてたんだった)。

惜しいので、ループ内の自身をスキン変数の表記のままで処理できるよう改良したのが次のコード。引数の取得や、コンテナ内のパースをPreSkinParseイベントで処理するように変更。

/*これはテストコードです*/
class NP_Container extends NucleusPlugin {
    function getName() { return 'Container'; }
    function getAuthor()  { return 'yu'; }
    function getURL() { return 'http://nucleus.datoka.jp/'; }
    function getVersion() { return '0.2'; }
    function getMinNucleusVersion() { return 330; }
    function getEventList() { return array( 'PreSkinParse' ); }

    function getDescription() { 
        return "test imprementation of container tag";
    }

    function supportsFeature($what) {
        switch($what){
            case 'SqlTablePrefix':
                return 1;
            default:
                return 0;
        }
    }

    function init() {
        $this->cnt = 0;
    }

    /* parse container tag */
    function event_PreSkinParse($data) { 
        $this->skintype = $data['type'];
        $search = '/<%Container\(begin([^)]*?)\)%>(.+?)<%Container\(end\)%>/s';
        $data['contents'] = preg_replace_callback($search, array(&$this, '_cb_preskinparse'), $data['contents']);
    }

    function _cb_preskinparse($m) {
        //retrieve skinvar arguments and set them to properties
        if ($m[1]) { 
            $params = explode(',', $m[1]);
            foreach ($params as $param) {
                list($type, $value) = explode(':', $param);
                switch ($type) {
                case 'mode':
                    $this->mode = $value;
                    break;
                case 'max':
                    $this->max = (int)$value;
                    break;
                }
            }
        }

        //parse skin data in container tag
        $handler = new ACTIONS($this->skintype);
        $parser = new PARSER(SKIN::getAllowedActionsForType($this->skintype), $handler);
        $handler->parser =& $parser;

        $parts = $this->_get_parts($m[2]); //expects 'header', 'body', and 'footer'
        ob_start();
        echo $parser->parse($parts['header']);
        while ($this->cnt++ < $this->max) $parser->parse($parts['body']);
        echo $parser->parse($parts['footer']);
        $buff = ob_get_contents();
        ob_end_clean();

        return $buff;
    }

    /* get template parts */
    function _get_parts($buff) {
        $parts = array();
        $data = preg_split("{<part name=\"([0-9a-zA-Z_-]+)\">|</part>}", $buff, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
        while ($data) {
            $type = array_shift($data);
            $type = trim($type);
            if ( empty($type) ) continue;
            $parts[$type] = array_shift($data);
        }
        return $parts;
    }

    /* parse var tag (not container tag) */
    function doSkinVar($skinType) {
        $params = func_get_args();

        foreach ($params as $param) {
            list($type, $value) = explode(':', $param);
            switch ($type) {
            case 'echo':
                if ($this->mode) 
                    echo $this->mode . $this->cnt; //test output
                else 
                    echo 'brahbrah';
                break;
            default:
            }
        }
    }
}

beginの後に続く引数の受け取りをPreSkinParseでやっとかなきゃいけないのがアレだけど、通常のスキン変数(MTでいうところの変数タグ)の処理はdoSkinVar()に従来通りに書ける。スキンへの記述も自然になった。

<%Container(begin,mode:local,max:3)%>
<part name="header">
<p>コンテナ内で他のスキン変数 <%if(category)%><%category(name)%><%else%>カテゴリ未選択<%endif%></p>
<dl>
<dt><%blogsetting(name)%></dt>
</part>
<part name="body">
<dd>ループ内の出力 <%Container(echo)%></dd>
</part>
<part name="footer">
</dl>
</part>
<%Container(end)%>

前バージョンと比べて処理が重くなるわけでもないし、なかなか良いかも。 将来的にコア側でこのへんをもっとスマートにサポートしてくれる仕組みができないかな。