$VERSION
=
'1.03'
;
sub
_uncolorized_length($)
{
my
$str
=
shift
;
$str
=~ s/\e \[ [^m]* m//xmsg;
return
length
$str
;
}
sub
_min_width($)
{
my
$str
=
shift
;
my
$min
;
for
my
$s
(
split
(/\s+/,
$str
)) {
my
$l
= _uncolorized_length
$s
;
$min
=
$l
if
not
defined
$min
or
$l
>
$min
;
}
return
$min
?
$min
: 1;
}
sub
_max_width($)
{
my
$str
=
shift
;
my
$len
= _uncolorized_length
$str
;
return
$len
?
$len
: 1;
}
sub
_max($$)
{
my
(
$a
,
$b
) =
@_
;
return
$a
if
defined
$a
and (not
defined
$b
or
$a
>=
$b
);
return
$b
;
}
sub
_wrap($$)
{
my
(
$width
,
$text
) =
@_
;
my
@lines
=
split
(/\n/,
$text
);
my
@w
= ();
for
my
$l
(
@lines
) {
push
@w
, @{_wrap_line(
$width
,
$l
)};
}
return
\
@w
;
}
sub
_wrap_line($$)
{
my
(
$width
,
$text
) =
@_
;
my
$width_m1
=
$width
-1;
my
@t
= (
$text
);
while
(1) {
my
$t
=
pop
@t
;
my
$l
= _uncolorized_length
$t
;
if
(
$l
<=
$width
){
push
@t
,
$t
;
return
\
@t
;
}
elsif
(
$t
=~ /^(.{0,
$width_m1
}\S)\s+(\S.*?)$/) {
push
@t
, $1;
push
@t
, $2;
}
elsif
(
$t
=~ /(.{
$width
,}?\S)\s+(\S.*?)$/) {
if
( _uncolorized_length $1 >
$width_m1
)
{
my
$left
=
substr
($1,0,
$width
);
my
$right
=
substr
($1,
$width
);
push
@t
,
$left
;
push
@t
,
$right
;
push
@t
, $2;
}
else
{
push
@t
, $1;
push
@t
, $2;
}
}
else
{
my
$left
=
substr
(
$t
,0,
$width
);
my
$right
=
substr
(
$t
,
$width
);
push
@t
,
$left
;
push
@t
,
$right
;
return
\
@t
;
}
}
return
\
@t
;
}
sub
_l_box($$)
{
my
(
$width
,
$text
) =
@_
;
my
$lines
= _wrap(
$width
,
$text
);
map
{
$_
.=
' '
x(
$width
-_uncolorized_length(
$_
)) }
@$lines
;
return
$lines
;
}
sub
_r_box($$)
{
my
(
$width
,
$text
) =
@_
;
my
$lines
= _wrap(
$width
,
$text
);
map
{
$_
= (
' '
x(
$width
-_uncolorized_length(
$_
)).
$_
) }
@$lines
;
return
$lines
;
}
sub
_distribution_f($)
{
my
$max_width
=
shift
;
return
log
(
$max_width
);
}
sub
_calculate_widths($$)
{
my
(
$self
,
$width
) =
@_
;
my
@widths
= ();
for
my
$r
(@{
$self
->{data}})
{
$r
->[0] eq
'data'
or
$r
->[0] eq
'head'
or
next
;
my
$cn
=0;
my
(
$max
,
$min
) = (0,0);
for
my
$c
(@{
$r
->[1]}) {
if
(
$self
->{fixed_widths}[
$cn
] )
{
$widths
[
$cn
][0] =
$self
->{fixed_widths}[
$cn
];
$widths
[
$cn
][1] =
$self
->{fixed_widths}[
$cn
];
}
else
{
$widths
[
$cn
][0] = _max(
$widths
[
$cn
][0], _min_width
$c
);
$widths
[
$cn
][1] = _max(
$widths
[
$cn
][1], _max_width
$c
);
}
$cn
++;
}
}
my
(
$total_min
,
$total_max
) = (0,0);
for
my
$c
(
@widths
) {
$total_min
+=
$c
->[0];
$total_max
+=
$c
->[1];
}
my
$extra_width
+=
scalar
grep
{
$_
->[0] eq
'|'
or
$_
->[0] eq
' '
}
(@{
$self
->{
format
}});
$total_min
+=
$extra_width
;
$total_max
+=
$extra_width
;
if
(
$total_max
<=
$width
) {
my
$cn
= 0;
for
my
$c
(
@widths
) {
$self
->{widths}[
$cn
]=
$c
->[1];
$cn
++;
}
$self
->{total_width} =
$total_max
;
}
else
{
my
@dist_width
;
ITERATION:
while
(1) {
my
$total_f
= 0.0;
my
$fixed_width
= 0;
my
$remaining
=0;
for
my
$c
(
@widths
) {
if
(
defined
$c
->[2]) {
$fixed_width
+=
$c
->[2];
}
else
{
$total_f
+= _distribution_f(
$c
->[1]);
$remaining
++;
}
}
my
$available_width
=
$width
-
$extra_width
-
$fixed_width
;
if
(
$available_width
<
$remaining
*5) {
$available_width
=
$remaining
*5;
$width
=
$extra_width
+
$fixed_width
+
$available_width
;
}
my
$cn
=-1;
COLUMN:
for
my
$c
(
@widths
) {
$cn
++;
next
COLUMN
if
defined
$c
->[2];
my
$w
= _distribution_f(
$c
->[1]) *
$available_width
/
$total_f
;
if
(
$c
->[0] >
$w
) {
$c
->[2] =
$c
->[0];
next
ITERATION;
}
if
(
$c
->[1] <
$w
) {
$c
->[2] =
$c
->[1];
next
ITERATION;
}
$dist_width
[
$cn
] =
int
(
$w
);
}
last
;
}
my
$cn
= 0;
for
my
$c
(
@widths
) {
$self
->{widths}[
$cn
]=
defined
$c
->[2] ?
$c
->[2] :
$dist_width
[
$cn
];
$cn
++;
}
}
}
sub
_render_rule($$)
{
my
(
$self
,
$char
) =
@_
;
my
$out
=
''
;
my
(
$col
,
$data_col
) = (0,0);
for
my
$c
(@{
$self
->{
format
}}) {
if
(
$c
->[0] eq
'|'
) {
if
(
$char
eq
'-'
) {
$out
.=
'+'
}
elsif
(
$char
eq
' '
) {
$out
.=
'|'
}
else
{
$out
.=
$char
}
}
elsif
(
$c
->[0] eq
' '
) {
$out
.=
$char
;
}
elsif
(
$c
->[0] eq
'l'
or
$c
->[0] eq
'L'
or
$c
->[0] eq
'r'
or
$c
->[0] eq
'R'
) {
$out
.= (
$char
)x(
$self
->{widths}[
$data_col
]);
$data_col
++;
}
$col
++;
}
return
$out
.
"\n"
;
}
sub
_render_data($$)
{
my
(
$self
,
$data
) =
@_
;
my
@rdata
;
my
(
$col
,
$data_col
) = (0,0);
my
$lines
=0;
my
@rows_in_column
;
for
my
$c
(@{
$self
->{
format
}}) {
if
( (
$c
->[0] eq
'l'
) or (
$c
->[0] eq
'L'
) ) {
my
$lb
= _l_box(
$self
->{widths}[
$data_col
],
$data
->[
$data_col
]);
$rdata
[
$data_col
] =
$lb
;
my
$l
=
scalar
@$lb
;
$lines
=
$l
if
$lines
<
$l
;
$rows_in_column
[
$data_col
] =
$l
;
$data_col
++;
}
elsif
( (
$c
->[0] eq
'r'
) or (
$c
->[0] eq
'R'
) ) {
my
$rb
= _r_box(
$self
->{widths}[
$data_col
],
$data
->[
$data_col
]);
$rdata
[
$data_col
] =
$rb
;
my
$l
=
scalar
@$rb
;
$lines
=
$l
if
$lines
<
$l
;
$rows_in_column
[
$data_col
] =
$l
;
$data_col
++;
}
$col
++;
}
my
$out
=
''
;
for
my
$l
(0..(
$lines
-1)) {
my
(
$col
,
$data_col
) = (0,0);
for
my
$c
(@{
$self
->{
format
}}) {
if
(
$c
->[0] eq
'|'
) {
$out
.=
'|'
;
}
elsif
(
$c
->[0] eq
' '
) {
$out
.=
' '
;
}
elsif
(
$c
->[0] eq
'L'
or
$c
->[0] eq
'R'
)
{
my
$start_print
=
$lines
-
$rows_in_column
[
$data_col
];
if
(
defined
$rdata
[
$data_col
][
$l
-
$start_print
]
and
$l
>=
$start_print
)
{
$out
.=
$rdata
[
$data_col
][
$l
-
$start_print
];
}
else
{
$out
.=
' '
x(
$self
->{widths}[
$data_col
]);
}
$data_col
++;
}
elsif
(
$c
->[0] eq
'l'
or
$c
->[0] eq
'r'
) {
if
(
defined
$rdata
[
$data_col
][
$l
]) {
$out
.=
$rdata
[
$data_col
][
$l
];
}
else
{
$out
.=
' '
x(
$self
->{widths}[
$data_col
]);
}
$data_col
++;
}
$col
++;
}
$out
.=
"\n"
;
}
return
$out
;
}
sub
_parse_format($$)
{
my
(
$self
,
$format
) =
@_
;
my
@f
=
split
(//,
$format
);
my
@format
= ();
my
@width
= ();
my
(
$col
,
$data_col
) = (0,0);
my
$wid
;
for
my
$f
(
@f
) {
if
(
$f
=~ /(\d+)/)
{
$wid
.=
$f
;
next
;
}
if
(
$f
eq
'l'
or
$f
eq
'L'
or
$f
eq
'r'
or
$f
eq
'R'
) {
$format
[
$col
] = [
$f
,
$data_col
];
$width
[
$data_col
] =
$wid
;
$wid
=
undef
;
$data_col
++;
}
elsif
(
$f
eq
'|'
or
$f
eq
' '
) {
$format
[
$col
] = [
$f
];
}
else
{
croak
"unknown column format: $f"
;
}
$col
++;
}
$self
->{
format
}=\
@format
;
$self
->{fixed_widths}=\
@width
;
$self
->{col}=
$col
;
$self
->{data_col}=
$data_col
;
}
sub
new($$)
{
my
(
$class
,
$format
) =
@_
;
croak
"new() requires one argument: format"
unless
defined
$format
;
my
$self
= {
col
=>
'0'
,
row
=>
'0'
,
data
=> [] };
bless
$self
,
$class
;
$self
->_parse_format(
$format
);
return
$self
;
}
sub
_preprocess_row_data($$)
{
my
(
$self
,
$data
) =
@_
;
my
$cn
= 0;
for
my
$c
(0..(
$#$data
)) {
$data
->[
$c
] =~ s/^\s+//m;
$data
->[
$c
] =~ s/\s+$//m;
}
}
sub
head($@)
{
my
(
$self
,
@data
) =
@_
;
scalar
@data
==
$self
->{data_col} or
croak
"number of columns must be $self->{data_col}"
;
$self
->_preprocess_row_data(\
@data
);
$self
->{data}[
$self
->{row}++] = [
'head'
, \
@data
];
}
sub
row($@)
{
my
(
$self
,
@data
) =
@_
;
scalar
@data
==
$self
->{data_col} or
croak
"number of columns must be $self->{data_col}"
;
$self
->_preprocess_row_data(\
@data
);
$self
->{data}[
$self
->{row}++] = [
'data'
, \
@data
];
}
sub
rule($$)
{
my
(
$self
,
$char
) =
@_
;
$char
=
'-'
unless
defined
$char
;
$self
->{data}[
$self
->{row}++] = [
'rule'
,
$char
];
}
sub
render($$)
{
my
(
$self
,
$width
) =
@_
;
$width
= 79
unless
defined
$width
;
$self
->_calculate_widths(
$width
);
my
$out
=
''
;
for
my
$r
(@{
$self
->{data}}) {
if
(
$r
->[0] eq
'rule'
) {
$out
.=
$self
->_render_rule(
$r
->[1]);
}
elsif
(
$r
->[0] eq
'head'
) {
$out
.=
$self
->_render_data(
$r
->[1]);
}
elsif
(
$r
->[0] eq
'data'
) {
$out
.=
$self
->_render_data(
$r
->[1]);
}
}
return
$out
;
}
1;