#!/usr/bin/perl
#! code perl
# justify all the lines to a predefined width (default 80)
# usage: 
# as with any other Perl script you can either 
# $ perl justifier.pl -wW [file]...
# or change the above line /usr/bin/perl to your local Perl binary
# and:
#
# $ chmod +x justifier.pl
# $ ./justifier.pl -wW [file]...
#
# where file is one or several filenames. If no filename is given,
# standard input is used.
# -wW is the line width, which is 80 by default.
#
# There are several options (some of them even useful), type
#
# $ ./justifier.pl -h
#
# to see them.
# 
# Notice that this program is none of either:
# a) the simplest one
# b) the fastest one
# c) the cleanest one
# but I did it quickly and it does the job for me. Want more? Pay.
# http://pfortuny.sdf-eu.org
#
use strict ;
use warnings ;

# "Global" variables.
my ($COL_WIDTH, $RANDOM_SPACE, $LEFTWARD_SPACE, 
    $IGNORE_LOOSE, $LOOSE) = (0) x 5;
my ($IGNORE_REGEXP, $PREPEND, ) = ("", "", ) ;
my $IGNORE_STOPS = 0 ;
# Parameter processing
while(@ARGV and $ARGV[0] =~ /^-/) {
    $_ = shift @ARGV;
    last if /^--$/;
    if    (/^-w(\d+)$/)          {$COL_WIDTH = $1 ;}
    elsif (/^-sr$/)              {$RANDOM_SPACE = 1 ;}
    elsif (/^-sl$/)              {$LEFTWARD_SPACE = 1 ;}
    elsif (/^-il(\d*)$/)         {$IGNORE_LOOSE = 1 ; $LOOSE = $1}
    elsif (/^-I(.*)$/)           {$IGNORE_REGEXP = eval {qr/$1/} or 
                                      die "BOGUS -I option: $1" ;}
    elsif (/^-is$/)              {$IGNORE_STOPS = 1 ;}
    elsif (/^-p(.*)/)            {$PREPEND = $1}  
    else { error_exit() ;}
}

$IGNORE_STOPS = qr"[]})\.:?!{([]$"  if $IGNORE_STOPS; # " for fancy highlighting
$COL_WIDTH ||= 80 ;
$COL_WIDTH -= length $PREPEND ;
$LOOSE ||= $COL_WIDTH * $IGNORE_LOOSE ;
my $line = "" ;
# main loop.
LINE:
    while(<>){
	# do not chomp if we simply need to copy
        my $start = 1;
        if($IGNORE_REGEXP) {
            if (/$IGNORE_REGEXP/o ){
                print $_ ;
                next LINE;
            }
        }
        my $output = "";
        # Text from Mac won't work in this system...
	# We probabaly want something like this s/\r?\n// ;
	# But also probably do not.
        chomp ;
	$line .= $_ ;
	($output, $line) = justify($start, $line);
	$start = 0 if $line ;
        while($line) { #was $line
	    print $output ;
	    print $PREPEND ;
            ($output, $line) = justify($start, $line);
        }
	# chomp the last part of the line and process it again, otherwise,
	# loose lines were always printed verbatim (which is not necessarily
	# desired)
	chomp $output ;
	($output, $line) = justify($start, $output ) ;
	print $output ;
}
 
# main subroutine: 
# justify($start, $line):
#
# INPUT:
#   $start: a boolean indicating whether the line is the start of a paragraph 
#        ($start = 1) or not.
#   $line:  a "line", which contains a string.
#
# OUTPUT: ($output, $overfull)
#
#   $output: a string of length less than or equal to $COL_WIDTH containing
#            a trimmed version of $line but for the added/removed spaces and 
#            possibly some prepended text $PREPEND.
#   $overfull: the rest of $line, once $output has been taken away from it.
#

sub justify {
    my $start = shift @_ ;
    my $local_line = shift @_ || "" ;
    my ($output, $overfull, $space) = ("", "", "") ;
    my %spaces = ("", "") ; 
    my @last_space = ("", ) ;
    my $col_width = $COL_WIDTH;
    # adjust column width for starting lines (which are not prepended by 
    # anything)
    $col_width += length $PREPEND if $start ;
    if($IGNORE_LOOSE and length($local_line) <= $LOOSE) {
        return( $local_line . "\n", "");
    }
    # kill trailing and starting spaces
    $local_line = join(" ", my @words = split(/\s+/,$local_line)) ;
    while($#words and length ("@words") > $col_width) {
        $overfull = (pop @words) . " " . $overfull ;
    }
    # now @words contains what is being output:
    if (($start and $IGNORE_STOPS) and "@words" =~ $IGNORE_STOPS) {
	return ("@words\n", $overfull) ;
    }
    if ($#words) {
        my $free_space = $col_width - length(join ("", @words)) ;
        $space = " " x int($free_space / $#words) ;
        @last_space = (" ") x ($free_space % $#words) ;
        %spaces = ();
        for(my $i = 0; $i <= $#last_space; $i++) {
          CHOOSE_J:
            my $j = $i ;
            if ($RANDOM_SPACE) {
                $j = int rand ($#words);
                if($spaces{$j}) {goto CHOOSE_J ;}
            }
            if ($LEFTWARD_SPACE) {
                $j = $#words - $i ; 
            }
            $spaces{$j} = " ";
        }
        my $i = 0 ;
        foreach my $word (0..$#words - 1){
            $output .= $words[$word] . $space ;
            $output .= ($spaces{$i} ? pop @last_space : "" ) ;
            $i++ ;
        }
    }
    @words = ("") unless @words ; # avoid nasty "uninitialized..." warnings
    $output .= ($output ? "@last_space" . pop (@words) : 
		pop (@words) ) . "\n" ;
    return ($output, $overfull) ;
}


#
# exit subroutine which prints usage information.
#
#! end perl

sub error_exit {
        (my $error = <<EOE) =~ s/^\s+//gm;
        Usage: justifier.pl -wW -sr -sl [[file], ...]

        where W is an integer numbers. Switches mean:

        -wW: column width (default 80)
        -sr: randomly shuffle outstanding blank space
        -sl: distribute outstanding blank space leftwards
        -il: ignore loose: lines whose length is less than
             the specified width will be printed verbatim.
	-is: ignore loose lines *ending* in a stop character:
	     any of []})\.:?!{([].
	     You usually want to set this switch on, otherwise,
	     the output seems "too uniform". However, it is off
	     by default because it is not correct to do this
	     "stricto sensu".
        -pT: prepend T (a string) before any "artificial" line:
             that is, after any newline which was not present
             in the original file. The width will be adjusted
             by the length of T.
        -IR: Ignore the regexp "R" (Perl regexp). Any line in which
             the regexp R is found will not be processed and will
             be printed verbatim.
        If no files are specified, standard input is processed.

        Example:

        \$ justifier.pl -sr -is -il40 -w50 -p'# ' -I'^[^#]'

        will justify at 50 width lines prepending the string "# "
        before each 'artificial' newline. If any line (true or artificial
        has length less than or equal to 40, it will not be justified). 
	Any input line not starting with a sharp sign will not be processed.
	Also, loose lines ending in .?![]:(){} are NOT justified.
        This example is useful for justifying **only the comment lines**
        of Perl (and shell) files.
EOE
    print $error ;
    exit(1) ;

}

