
# 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 <p> tag, we want to stick that at the
		# beginning of each column after the first
	if ($text =~ /^\s*(<p[^>]*>)/) {
		$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 <p> 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#^</#) && (substr($text, $tagend - 1, 1) ne '/')) {
							# find out the name of the tag
						if ($fulltag =~ /^<([^ >]+)/) {
							my $tag = $1;
								# ignore <p> 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, "</$tag", $pos);
								if ($closestart > -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;
