# Columnize.pl # Movable Type plugin tags for splitting text into multiple columns # by Kevin Shay # http://www.staggernation.com/mtplugins/ # last modified September 21, 2004 package MT::Plugin::Columnize; use strict; use vars qw( $VERSION ); $VERSION = '1.11'; use MT; use MT::Template::Context; eval{ require MT::Plugin }; unless ($@) { my $plugin = { name => "Columnize $VERSION", description => 'Split your entries or listings into multiple columns on the page.', doc_link => 'http://www.staggernation.com/mtplugins/Columnize' }; MT->add_plugin(new MT::Plugin($plugin)); } MT::Template::Context->add_container_tag('Columnize' => sub{&_hdlr_columnize;}); MT::Template::Context->add_container_tag('ColumnizeText' => sub{&_hdlr_columnize_text;}); MT::Template::Context->add_container_tag('ColumnizeColumn' => sub{&_hdlr_columnize_column;}); MT::Template::Context->add_container_tag('ColumnizeSpacer' => sub{&_hdlr_columnize_spacer;}); MT::Template::Context->add_tag('ColumnizePortion' => sub{&_hdlr_columnize_portion;}); MT::Template::Context->add_tag('ColumnizeColNum' => sub{&_hdlr_columnize_col_num;}); MT::Template::Context->add_tag('ColumnizeBreakHere' => sub{&_hdlr_columnize_break_here;}); use vars qw($break_here_str); $break_here_str = '~BH~'; sub _hdlr_columnize { # handler for MT container tag for splitting text into columns # this is just a wrapper that stashes the arguments and builds its contents; # the nested tags do the actual work # args to tag: # cols: number of columns # min_chars (optional): character count; if tag's contents are below this # length, we won't split the text into columns (we'll still print the # column wrapper, but just print a single column) my ($ctx, $args, $cond) = @_; return $ctx->error('No number of columns passed') unless defined($args->{'cols'}); ((my $cols = $args->{'cols'}) > 0) || return $ctx->error('Invalid number of columns passed'); $ctx->stash('col_cols', $cols); $ctx->stash('col_min_chars', defined($args->{'min_chars'}) ? $args->{'min_chars'} : 0); defined(my $text = $ctx->stash('builder')->build($ctx, $ctx->stash('tokens'), $cond)) || return $ctx->error($ctx->errstr); return $text; } sub _hdlr_columnize_text { # handler for MT container tag that delimits the text that should be broken # into columns; doesn't output anything, just stashes its contents my ($ctx, $args, $cond) = @_; defined(my $text = $ctx->stash('builder')->build($ctx, $ctx->stash('tokens'), $cond)) || return $ctx->error($ctx->errstr); $ctx->stash('col_text', $text); # if the text starts with a

tag, we want to stick that at the # beginning of each column after the first if ($text =~ /^\s*(]*>)/) { $ctx->stash('col_prepend', $1); } $ctx->stash('col_delim', ($text =~ /$break_here_str/) ? $break_here_str : ' '); return ''; } sub _hdlr_columnize_column { # handler for MT container tag that delimits the HTML (i.e. a table cell) # that will be printed once for each column my ($ctx, $args, $cond) = @_; defined(my $cols = $ctx->stash('col_cols')) || return $ctx->error('Not called from within MTColumnize container'); defined(my $text = $ctx->stash('col_text')) || return $ctx->error('MTColumnizeText must be called before MTColumnizeColumn'); if (length($text) < $ctx->stash('col_min_chars')) { $cols = 1; } # get delimiter on which we should break, either a space or the # placeholder inserted by MTColumnizeBreakHere my $delim = $ctx->stash('col_delim'); my $len = length($delim); # store initial builder and tokens so we can repeat them my $builder = $ctx->stash('builder'); my $tokens = $ctx->stash('tokens'); my $output = ''; # split text into columns. approach is to loop for the number # of columns we want, each time grabbing a roughly equal chunk for my $i (0 .. $cols - 1) { my ($portion, $next, $prev); # if the text started with a

tag, we want to stick that at the # beginning of each column after the first if (($i > 0) && defined(my $prepend = $ctx->stash('col_prepend'))) { $portion = $prepend; } # how many columns do we have left? my $remaining = ($cols - $i); # last column, no need to calculate anything if ($remaining == 1) { $portion .= $text; } else { # where would we split, ideally? my $pos = int(length($text) / $remaining); # but we can't split there unless it's a delimiter if (substr($text, $pos, $len) ne $delim) { # so figure out the position of the nearest delimiter if ($text !~ /$delim/) { # if no delimiter is found, that probably means they # were using the "break here" string within the text # itself; now we can change the delimiter to a space # for subsequent columns if ($delim ne ' ') { $delim = ' '; $len = 1; } } my $next = index($text, $delim, $pos); my $prev = rindex($text, $delim, $pos); if ($next + $prev == -2) { # no spaces? just leave it alone } elsif ($next == -1) { $pos = $prev; } elsif ($prev == -1) { $pos = $next; } else { $pos = (($next - $pos) < ($pos - $prev)) ? $next : $prev; } } # and now, very non-robust code to try to avoid splitting a tag # (only if we're breaking on space; if user is using the # MTColumnizeBreakHere feature, we assume they're not # going to put it in the middle of a tag) if ($delim eq ' ') { my $tagstart = rindex($text, '<', $pos); my $tagend = rindex($text, '>', $pos); # first, make sure we're not actually in the middle of a tag # (between < and >) if ($tagstart > $tagend) { $tagend = index($text, '>', $pos); if ($tagend > -1) { my $sp = index($text, ' ', $tagend); $pos = ($sp > -1) ? $sp : $tagend; } } if ($tagstart + $tagend > -2) { # grab the last tag that precedes our position my $fulltag = substr($text, $tagstart, $tagend - $tagstart + 1); # check that it's not a closing tag or self-contained tag if (($fulltag !~ m#^]+)/) { my $tag = $1; # ignore

tags if (lc($tag) ne 'p') { # find the closing tag that matches this tag # (won't work if it's nested, sorry) my $closestart = index($text, " -1) { my $closeend = index($text, '>', $closestart); if ($closeend > -1) { $pos = $closeend; } } } } } } # whew, glad that's over with } $portion .= substr($text, 0, $pos); $text = substr($text, $pos + $len); } $portion =~ s/$break_here_str//g; # prepare the values for the nested tags to print $ctx->stash('col_portion', $portion); $ctx->stash('col_i', $i + 1); defined(my $column = $builder->build($ctx, $tokens, $cond)) || return $ctx->error($ctx->errstr); $output .= $column; } return $output; } sub _hdlr_columnize_spacer { # handler for MT container tag that delimits some HTML to be printed # after each column except the last (i.e. a spacer cell) my ($ctx, $args, $cond) = @_; defined(my $cols = $ctx->stash('col_cols')) || return $ctx->error('Not called from within MTColumnize container'); defined(my $i = $ctx->stash('col_i')) || return $ctx->error('Not called from within MTColumnizeColumn container'); return '' if ($i == $cols); defined(my $text = $ctx->stash('builder')->build($ctx, $ctx->stash('tokens'), $cond)) || return $ctx->error($ctx->errstr); return $text; } sub _hdlr_columnize_col_num { # handler for MT tag that prints the number of the current column my ($ctx, $args) = @_; defined(my $i = $ctx->stash('col_i')) || return $ctx->error('Not called from within MTColumnizeColumn container'); return $i; } sub _hdlr_columnize_portion { # handler for MT tag that prints a one-column portion of the columnized text my ($ctx, $args) = @_; defined(my $portion = $ctx->stash('col_portion')) || return ''; return $portion; } sub _hdlr_columnize_break_here { # handler for MT tag that inserts a placeholder where the user wants column # breaks to go (instead of breaking at spaces between words) return $break_here_str; } 1;