Skip to content

English plugin dev 2 2

semuel edited this page Dec 15, 2011 · 21 revisions

Developing Block-Tag Plugins

But what are block tags?

Examples of block tags are <MTBlogs><MTEntries><MTTags>. They are all basically loop tags, setting the stage (and the context) for other tags, zero times or more.
Unlike function tags, these tags need to be matched with a closing tag. for example: <MTBlogs>~</MTBlogs>

Other usage for these tags are as transformers of the content. They can accept data in the block, an embed it in HTML of their own, (mt:app tags are doing that) transform it to something else, or doing some other operation based on it. (mt:SetVarBlock tag is saving the content of the block to the named variable)

Operation

  • Collect data and objects based on Tag parameters and current context
  • For each round of the loop, set the context for the tags inside
  • Upon completion, reset back any change, and return

The difference between Perl and PHP is that in Perl all this should happen in one function call, and in PHP the function is called once in the beginning, once in the end, and between loops.

Test Case Specification

Plugin Usage

  • The block tag name: <MTCategoryBreadcrumbs>
  • Function tag to display the breadcrumbs: <MTCategoryBreadcrumbTab>
  • Tags will be use as this example:
    <p><MTCategoryBreadcrumbs><MTCategoryBreadcrumbTab></MTCategoryBreadcrumbs></p>
  • 利用対象はブログ記事のみ
  • IF there is not category set, display “Top” like, to the Archive home
    [ Top ]
  • If there is a category, display breadcrumbs as:
    [ Top ] < [ foo ] < [ subfoo ]
  • カテゴリのアーカイブが作成されていない場合、上記"foo"のようにリンクは張られない
  • カテゴリのアーカイブが作成されている場合、上記"subfoo"のようにカテゴリアーカイ
    ブへのリンクが張られる
  • This plugin handles on the ‘main’ category of an entry, the one marked in a star (☆)

Test Case 0 (00-compile.t)

Let’s make sure that each module is loaded without problems

use strict;
use lib qw( t/lib lib extlib );
use warnings;
use MT;
use Test::More tests => 5;
use MT::Test;

ok(MT->component ('MyPlugin05'), "MyPlugin05 plugin loaded correctry");

require_ok('MyPlugin05::L10N');
require_ok('MyPlugin05::L10N::ja');
require_ok('MyPlugin05::L10N::en_us');
require_ok('MyPlugin05::Tags');

1;

Text Case 1 (01-tags.t)

We have three spec items for this test:

  1. For an empty string return an empty string
  2. For an entry that have a sub-category, return breadcrumbs
  3. For an entry that does not have a category, return link to the top
途中改行されていますが、実際は9行です。

#===== Edit here
my $test_json = <<'JSON';
[
{ "r" : "1", "t" : "", "e" : ""},
{ "r" : "1", "t" :
 "<mt:Entries id=\"7\"><MTCategoryBreadcrumbs><MTCategoryBreadcrumbTab></MTCategoryBreadcrumbs></mt:Entries>",
 "e" : "[ <a href=\"http://narnia.na/nana/archives/\">Top</a> ]
 &lt; [ <a href=\"http://narnia.na/nana/archives/foo/index.html\">foo</a> ]
 &lt; [ <a href=\"http://narnia.na/nana/archives/foo/subfoo/index.html\">subfoo</a> ]"},
{ "r" : "1", "t" :
 "<mt:Entries id=\"8\"><MTCategoryBreadcrumbs><MTCategoryBreadcrumbTab></MTCategoryBreadcrumbs></mt:Entries>",
 "e" : "[ <a href=\"http://narnia.na/nana/archives/\">Top</a> ]"}
]
JSON
#=====
 

Plugin Development (Perl)

We create our plugin based on MyPlugin05 written in the previous section

config.yaml

Similar to function tags and modifier tags, bloc tags are declared by “tags”=> “block”=> “tag name”=> $PluginName::HandlerName

id: MyPlugin06
name: <__trans phrase="Sample Plugin Block tag">
version: 1.0
description: <__trans phrase="_PLUGIN_DESCRIPTION">
author_name: <__trans phrase="_PLUGIN_AUTHOR">
author_link: http://www.example.com/about/
doc_link: http://www.example.com/docs/
l10n_class: MyPlugin06::L10N

tags:
    block:
        CategoryBreadcrumbs: $MyPlugin06::MyPlugin06::Tags::hdlr_categorybreadcrumbs
    function:
        CategoryBreadcrumbTab: $MyPlugin06::MyPlugin06::Tags::hdlr_categorybreadcrumbtab

L10N.pm

package MyPlugin06::L10N;
use strict;
use base 'MT::Plugin::L10N';

1;

L10N/en_us.pm

package MyPlugin06::L10N::en_us;

use strict;
use base 'MyPlugin06::L10N';
use vars qw( %Lexicon );

%Lexicon = (
    '_PLUGIN_DESCRIPTION' => 'Sample block tag',
    '_PLUGIN_AUTHOR' => 'Plugin author',
);

1;

L10N/ja.pm

package MyPlugin06::L10N::ja;

use strict;
use base 'MyPlugin06::L10N::en_us';
use vars qw( %Lexicon );

%Lexicon = (
    'Sample Plugin Block tag' => 'サンプルプラグイン ブロックタグ',
    '_PLUGIN_DESCRIPTION' => 'ブロックタグ テストプラグイン',
    '_PLUGIN_AUTHOR' => 'プラグイン作者',
);

1;

Tags.pm

package MyPlugin06::Tags;
use strict;

sub hdlr_categorybreadcrumbs {
    my ($ctx, $args, $cond) = @_;

    my $blog = $ctx->stash('blog') || return;
    my $entry = $ctx->stash('entry')
        || $ctx->error(MT->translate('You used an [_1] tag outside of the proper context.', 'CategoryBreadcrumbs'));
    my $cat = $entry->category() || return _hdlr_top_link($blog);

    my @categories = ();
    while (1){
        if ($cat->parent() == 0) {
            push (@categories, $cat);
            last;
        }
        push (@categories, $cat);
        $cat = MT::Category->load($cat->parent());
    }
    @categories = reverse @categories;

    my $out = _hdlr_top_link($blog);
    for my $category (@categories) {
        local $ctx->{__stash}{category} = $category;

        my $tokens = $ctx->stash('tokens');
        my $builder = $ctx->stash('builder');

        $out .= $builder->build( $ctx, $tokens, $cond)
            || return $ctx->error( $builder->errstr );
    }

    return $out;
}

sub hdlr_categorybreadcrumbtab {
    my ($ctx, $args) = @_;

    my $blog = $ctx->stash('blog') || return;
    my $category = $ctx->stash('category')
        || $ctx->error(MT->translate('You used an [_1] tag outside of the proper context.', 'CategoryBreadcrumbsTab'));

    require MT::Util;
    my $url = $blog->archive_url;
    $url .= '/' unless $url =~ m!/$!;
    $url .= MT::Util::archive_file_for(undef, $blog, 'Category', $category);

    my $count = MT::Placement->count({category_id => $category->id});

    my $anchor_start = '';
    my $anchor_end = '';
    if ( $url && $count ) {
        $anchor_start = '<a href="' . $url . '">';
        $anchor_end = '</a>';
    }

    my $label = $category->label;
    my $out = ' &lt; [ ' . $anchor_start . $label . $anchor_end. ' ]';

    return $out;
}

sub _hdlr_top_link {
    my ($blog) = @_;
    my $blog_url = $blog->archive_url;

    return "[ <a href=\"$blog_url\">Top</a> ]";
}

1;

Commentary

  • Package declaration
    Declaring the name of the package, and using “use strict;” as always

  • Handler function declaration for <MTCategoryBreadcrumbs>
    When the handler is called, it receives “$ctx”, “$args” as parameters.

  • Retrieving the necessary objects from the context ($ctx)
    At line 7, $ctx->stash('blog') retrieve the blog object. If there is no such blog active, return quietly.
    At lines 8~10 $ctx->stash('entry') retrieve the active entry. if there is no active entry, rise an error.
    After that, we ask for the main category of the entry. if there is no such set, we return only the top link, as specify in the spec. (the _hdlr_top_link($blog) function will be explained later)

  • Building Category hierarchical list
    As done in previous chapters, we collect the category and all its parents to a list. and then we reverse it, so we can display it in the desired order

  • Processing the inner block
    In line 23, we setup the Top link
    Then for every category in the array, we set this category in the context (using the “local” keyword ensuring that it will automatically be deleted afterwards)
    Then we execute the block between the opening tag and closing tag of this block tag, for each category. And if this execution ended in error, we propagate this error to the caller

  • End of the tag subroutine
    Return the string that we built

  • Handler function declaration for <MTCategoryBreadcrumbTab>
    When the handler is called, it receives “$ctx”, “$args” as parameters.

  • Retrieving the necessary objects from the context ($ctx)
    As written before, $ctx->stash('blog') retrieve the blog object. If there is no such blog active, return quietly.
    $ctx->stash('category') retrieve the category for which we need to print a link to. if there is no set category, rise an error.

  • Getting the category archive URL
    Using MT::Util::archive_file_for function to build the URL of the category archive

  • Check for category usage
    We want to display a link to the category page only if someone is actually using this category. (this can be a parent category that nobody uses directly. and in that case the category page is not created)

  • Building the “a” tag (link/anchor)
    If this category is used, then we want to add an “a” tag around the category name

  • Category label creating
    In line 58, we retrieve the category display name
    And in line 59 the HTML (such as < [ <a href=“http://www.example.com/foo/subfoo/”> ]) is created and returned

  • Create a link to the top of the breadcrumbs
    This is an internal function, used to create the Top link, that points to the archive path of the blog

Plugin Development (PHP)

block.mtcategorybreadcrumbs.php

OK, this one was really complicated. We have a block tag, whose name is MTCategoryBreadcrumbs. on the other hand, it is PHP. How in the world we will call the file that holds the tag code?
After three days of meetings, fueled by gallons of green tea and weird snacks, an answer have emerged: block.mtcategorybreadcrumbs.php

<?php
    function smarty_block_mtcategorybreadcrumbs ($args, $content, &$ctx, &$repeat) {
        $localvars = array('_categories', '_categories_counter', 'category');

        $blog = $ctx->stash('blog');
        if (!$blog) {
            return;
        }
        $entry = $ctx->stash('entry');
        if (!$entry) {
            return;
        }
        $cat = $entry->category();
        if (!$cat) {
            return _hdlr_top_link($blog);
        }

        if (!isset($content)) {
            $ctx->localize($localvars);

            while (1) {
                if ($cat->parent == 0) {
                    $categories[] = $cat;
                    break;
                }
                $categories[] = $cat;
                $category_id = $cat->category_parent;
                $cat = $ctx->mt->db()->fetch_category($category_id);
            }
            $categories = array_reverse($categories);
            $ctx->stash('_categories', $categories);
            $counter = 0;
        } else {
            $categories = $ctx->stash('_categories');
            $counter = $ctx->stash('_categories_counter');
        }


        if ($counter < count($categories)) {
            $category = $categories[$counter];
            $ctx->stash('category', $category);
            $ctx->stash('_categories_counter', $counter + 1);
            $repeat = true;
        } else {
            $ctx->restore($localvars);
            $repeat = false;
        }
        if ($counter == 1) {
            $content = _hdlr_top_link($blog) . $content; 
        }
        return $content;
    }

    function _hdlr_top_link ($blog) {
        $blog_url = $blog->archive_url();

        return "[ <a href=\"$blog_url\">Top</a> ]";
    }
?>

Block-tag function logic

To summarize:

  1. this function will be called once without content (for setup)
  2. As long as the function set $repeat to 1 do:
    1. the inner block will parsed
    2. the function will be called again with the resulting content
    3. the output of the function will be appended to the document

Commentary

  • Handler Function Declaration (<MTCategoryBreadcrumbs>)
    The function name is according the the “smarty” framework, smarty_block_mtcategorybreadcrumbs. The function will be called with these parameters: “$args”, “$content”, “$ctx”, “$repeat”
    This code is quite different from the Perl one

  • Retrieve objects from the Context ($ctx)
    Retrieve the blog and entry objects from the context, and return silently if they are not available.
    Then fetch the entry category. If it does not have one, return the ([ Top ]) link

  • Initial values and settings
    In line 18 there is a check if we received content or not.
    If we have not got content, this is the first time that the function was called for this block, and we need to build the category list. We also set a counter, to remember where we are
    If we got content, then this is not the first time, so we just retrieve the variables from the context.

  • main loop
    If we still haven’t exceed the number of categories in the array, put the relevant category in the context, and advance the counter by 1.
    If the counter is bigger the the array, we are done. clean up the context (which we localized in line 19) by calling restore.
    The repeat variable tells the template engine if this blog should be done again. so we set it accordingly.

  • End of the loop
    If this is after the first round, stick the ‘Top’ link in the beginning of the content. otherwise, return the content as it is.

  • Create a link to the top
    A helper function, like the one that was in the Perl implementation

function.mtcategorybreadcrumbtab.php

This a function tag, and we need a file name for it. As we don’t want to overload our precious super computer, we will just take the last file name for function tag, replace the tag name with mtcategorybreadcrumbtab, and hope that it will be OK: function.mtcategorybreadcrumbtab.php

<?php
    function smarty_function_mtcategorybreadcrumbtab ($args, &$ctx) {

        $blog = $ctx->stash('blog');
        $category = $ctx->stash('category');

        $args['blog_id'] = $ctx->stash('blog_id');
        if (!$category) return '';
        $url = $ctx->mt->db()->category_link($category->category_id, $args);
        $blog = $ctx->stash('blog');
        $index = $ctx->mt->config('IndexBasename');
        $ext = $blog->blog_file_extension;
        if ($ext) $ext = '.' . $ext; 
        $index .= $ext;
        $url = preg_replace('/\/(#.*)?$/', "/$index\$1", $url);

        $cat_list[] = $category->category_id;
        $placements = $ctx->mt->db()->fetch_placements(array('category_id' => $cat_list));
        if ($placements[0]) {
            $count = 1;
        }

        $anchor_start = '';
        $anchor_end = '';
        if ( $url && $count ) {
            $anchor_start = '<a href="' . $url . '">';
            $anchor_end = '</a>';
        }

        $label = $category->label;
        $out = ' &lt; [ ' . $anchor_start . $label . $anchor_end. ' ]';
 
        return $out;
    }
?>

Commentary

  • Function name according to the smarty framework, and getting two parameters: “$args” and “$ctx”

  • Retrieving objects from the context: we need the blog object and the category object (that the MTCategoryBreadcrumbs block tag set for us)

  • Getting the category archive URL
    In Perl we have a utility function “MT::Util::archive_file_for()” that calculate this, but not in PHP. The following code was stolen from the <MTCategoryArchiveLink> tag ($MT_DIR/php/lib/function.mtcategoryarchivelink.php)

  • Does this category in use?
    Count in how many places this category is used, by loading MT::Placement objects for this category. Actually, we don’t really need to count, just check the at least one placement exists

  • Prepare the “a” tag
    If there is at least one placement, we want to wrap the category name in an “a” tag. If not, we will display only the category name without a link. For this, we have $anchor_start and $anchor_end that are initialized to empty strings, and is there is a placement then are set to the ‘a’ tag

  • Building the output
    Now all that was left is to build the output string together, with the category label, and return it

Directory Structure

$MT_DIR/
|__ plugins/
   |__ MyPlugin06/
      |__ config.yaml
      |__ lib/
      |  |_ MyPlugin06/
      |     |__ L10N.pm
      |     |_ L10N/
      |     |  |_ en_us.pm
      |     |  |_ ja.pm
      |     |_ Tags.pm
      |__ php/
      |  |_block.mtcategorybreadcrumbs.php
      |  |_function.mtcategorybreadcrumbtab.php
      |__ t/
         |_00-compile.t
         |_01-tags.t

Running the tests

$ prove plugins/MyPlugin06/t/*.t
plugins/MyPlugin06/t/00-compile.t .. ok   
plugins/MyPlugin06/t/01-tags.t ..... ok   
All tests successful.
Files=2, Tests=12, 44 wallclock secs ( 0.07 usr  0.18 sys + 17.69 cusr  8.18 csys = 26.12 CPU)
Result: PASS

Now that the tests pass (it is so easy – writing tests, then code, then the tests pass! right? right?) we can include these tags in our blog templates. Go to the entry template, under “<div id=”alphe-inner">", and add “<p><MTCategoryBreadcrumbs><MTCategoryBreadcrumbTab></MTCategoryBreadcrumbs></p>”. publish (either statically or dynamically) to see the results

  • Entry Template
... snip ..
<div id="alpha">
  <div id="alpha-inner">
  <p><MTCategoryBreadcrumbs><MTCategoryBreadcrumbTab></MTCategoryBreadcrumbs></p>
... snip ..
  • Output page
(We broke the actual output to three lines for readability)

... snip ..
<div id="alpha">
  <div id="alpha-inner">
  <p>[ <a href="http://narnia.na/test5/">Top</a> ] &lt; [ foo ]
 &lt; [ <a href="http://narnia.na/test5/foo/subfoo/index.html">subfoo</a> ]</p>
... snip ..

Summary

Block tags are a nice tool to load can pass data to other tags, without polluting the global context. Also, the Perl interface is quite intuitive.

The PHP interface, on the other hand, is a bit bothersome. Still, please try and support it.

Also, while the block tags can change the layout of the block inside them, maybe using CSS, if possible, can give easier and more maintainable solution.

Plugin Download

MyPlugin06.zip(7.61KB)

Navigation

Prev:Developing Function Tags << Index >> Next:Developing Conditional Tags Plugins

Clone this wiki locally