package HTML::LoL::Special;
sub new {
my($type, $str) = @_;
bless \$str, $type;
}
package HTML::LoL;
use strict;
use base 'Exporter';
use vars qw(@ISA @EXPORT $VERSION);
$VERSION = '1.3';
@EXPORT = qw(hl hl_noquote hl_requote hl_entity hl_bool hl_preserve);
use constant TABWIDTH => 8;
use HTML::Entities;
use HTML::Tagset;
my $hl_bool_yes = new HTML::LoL::Special('bool_yes');
my $hl_bool_no = new HTML::LoL::Special('bool_no');
my $hl_noquote = new HTML::LoL::Special('noquote');
my $hl_requote = new HTML::LoL::Special('requote');
my $hl_preserve = new HTML::LoL::Special('preserve');
sub is_bool_yes {
my $x = shift;
return UNIVERSAL::isa($x, 'HTML::LoL::Special') && ($$x eq 'bool_yes');
}
sub is_bool_no {
my $x = shift;
return UNIVERSAL::isa($x, 'HTML::LoL::Special') && ($$x eq 'bool_no');
}
sub is_noquote {
my $x = shift;
return UNIVERSAL::isa($x, 'HTML::LoL::Special') && ($$x eq 'noquote');
}
sub is_requote {
my $x = shift;
return UNIVERSAL::isa($x, 'HTML::LoL::Special') && ($$x eq 'requote');
}
sub is_preserve {
my $x = shift;
return UNIVERSAL::isa($x, 'HTML::LoL::Special') && ($$x eq 'preserve');
}
# elements inside which it is OK to add whitespace
my %hl_wsok;
map { $hl_wsok{$_} = 1 } qw(area col colgroup frame frameset
head html object table tr);
# elements whose layout should not be altered
my %hl_pre;
map { $hl_pre{$_} = 1 } qw(pre style script textarea);
sub _emit {
my($cb, $str, $columnref) = @_;
my $result = &$cb($str);
if ($str =~ /.*\n([^\n]*)$/s) {
$str = $1;
$$columnref = 0;
}
my @s = split(/\t/, $str);
foreach my $s (@s) {
$$columnref += length($s);
}
if (@s > 1) {
$$columnref += (TABWIDTH * (@s - 1));
$$columnref = int($$columnref / TABWIDTH);
++$$columnref;
$$columnref *= TABWIDTH;
}
return $result;
}
sub _str {
my($cb, $str, $depth, $columnref, $wsokref, $pre, $noquote) = @_;
my $result;
$str = &encode_entities($str) unless $noquote;
if ($pre) {
$result = &_emit($cb, $str, $columnref);
} else {
my $leading_ws = ($str =~ /^\s/s);
my $trailing_ws = ($str =~ /\s$/s);
$str =~ s/^\s+//s;
$str =~ s/\s+$//s;
my @words = split(/\s+/, $str);
if (@words) {
$$wsokref ||= $leading_ws;
foreach my $word (@words) {
if ($$wsokref) {
if (($$columnref > 0)
&& ((1 + length($word) + $$columnref) > 72)) {
$result = &_emit($cb, ("\n" . (' ' x ($depth + 1))), $columnref);
} else {
$result = &_emit($cb, ' ', $columnref);
}
}
$result = &_emit($cb, $word, $columnref);
$$wsokref = 1;
}
} elsif ($leading_ws || $trailing_ws) {
$result = &_emit($cb, ' ', $columnref);
}
$$wsokref = $trailing_ws;
}
return $result;
}
sub _node {
my($cb, $node, $depth, $columnref, $wsokref, $pre, $noquote) = @_;
my $result;
my @node = @$node;
my $tag = $node[0];
my $empty;
if (&is_noquote($tag)) {
$noquote = 1;
undef $tag;
} elsif (&is_requote($tag)) {
$noquote = 0;
undef $tag;
} elsif (&is_preserve($tag)) {
$pre = 1;
undef $tag;
} else {
$tag = lc($tag);
$empty = $HTML::Tagset::emptyElement{$tag};
$pre ||= $hl_pre{$tag};
}
if ($$wsokref && !$pre) {
$result = &_emit($cb, ("\n" . (' ' x $depth)), $columnref);
}
if (defined($tag)) {
$result = &_emit($cb, "<$tag", $columnref);
foreach my $content (@node[1 .. $#node]) {
next unless ref($content) eq 'HASH';
foreach my $hashitem (keys %$content) {
my $val = $content->{$hashitem};
if (&is_bool_yes($val)) {
$result = &_emit($cb, " $hashitem", $columnref);
} elsif (&is_bool_no($val)) {
# do nothing
} elsif (ref($val) eq 'ARRAY') {
# the caller wants the value interpolated literally
$result = &_emit($cb,
sprintf(' %s=%s', $hashitem, $val->[0]),
$columnref);
} else {
$result = &_emit($cb,
sprintf(' %s="%s"', $hashitem,
&encode_entities($val)),
$columnref);
}
}
}
$result = &_emit($cb, ">", $columnref);
$$wsokref = $hl_wsok{$tag};
}
foreach my $content (@node[1 .. $#node]) {
my $ref = ref($content);
next if ($ref eq 'HASH');
if ($ref eq 'ARRAY') {
$result = &_node($cb, $content, $depth + 1, $columnref, $wsokref,
$pre, $noquote);
} else {
$result = &_str($cb, $content, $depth + 1, $columnref, $wsokref,
$pre, $noquote);
}
$$wsokref ||= $hl_wsok{$tag} if defined($tag);
}
if (defined($tag) && !$empty) {
if ($$wsokref) {
$result = &_emit($cb, ("\n" . (' ' x $depth)), $columnref);
}
$result = &_emit($cb, "$tag>", $columnref);
$$wsokref = 0;
}
return $result;
}
sub hl {
my $cb = $_[0];
my $column = 0;
my $wsok = 0;
my $result;
foreach my $elt (@_[1 .. $#_]) {
if (ref($elt)) {
$result = &_node($cb, $elt, 0, \$column, \$wsok, 0, 0);
} else {
$result = &_str($cb, $elt, 0, \$column, \$wsok, 0, 0);
}
}
return $result;
}
sub hl_noquote { [$hl_noquote => @_]; }
sub hl_requote { [$hl_requote => @_]; }
sub hl_preserve { [$hl_preserve => @_]; }
sub hl_entity { [$hl_noquote => map { "&$_;" } @_]; }
sub hl_bool { $_[0] ? $hl_bool_yes : $hl_bool_no }
1;
__END__
=head1 NAME
HTML::LoL - construct HTML from pleasing Perl data structures
=head1 SYNOPSIS
use HTML::LoL;
&hl(sub { print shift },
[body => {bgcolor => 'white'},
[p => 'Document body', ...], ...]);
See EXAMPLE section below.
=head1 DESCRIPTION
This module allows you to use Perl syntax to express HTML. The function
C converts Perl list-of-list structures into HTML strings.
The first argument to C is a callback function that's passed one
argument: a fragment of generated HTML. This callback is invoked repeatedly
with successive fragments until all the HTML is generated; the callback is
responsible for assembling the fragments in the desired output location (e.g.,
a string or file).
The remaining arguments to C are Perl objects representing HTML,
as follows:
=over 4
=item [B, ...]
B is a string (the name of an HTML element); remaining list items are any
of the forms described herein. Corresponds to
EBE...E/BE. If B is an "empty element"
according to C<%HTML::Tagset::emptyElement>, then the E/BE is
omitted.
=item [B => {B => B, B => B, ...}, ...]
Corresponds to EB B="B" B="B"
...E...E/BE.
(As above, E/BE is omitted if B is an "empty element.")
Each B is a string. Each B is
either a string, in which case the value gets HTML-entity-encoded when copied
to the output, or a list reference containing a single string (viz. [B])
in which case the value is copied literally.
Finally, for boolean-valued attributes, B may be C,
where BOOLEAN is a Perl expression. If BOOLEAN is true, the attribute is
included in the output; otherwise it's omitted.
=item Any string
Strings are copied verbatim to the output after entity-encoding.
=item C
Suppresses entity-encoding of its arguments.
=item C
Reenables entity-encoding of its arguments (use it inside a call to
C).
=item C
Normally, HTML::LoL tries to optimize the whitespace in the HTML it emits
(without changing the meaning of the HTML). This suppresses that behavior
within its arguments.
=item C
Includes the HTML character-entity named NAME.
=back
The return value of C is the result of the last call to the callback
function. This means it's possible to write
&hl(sub { $accumulator .= shift }, ...)
to have C return a string containing the completely rendered HTML.
=head1 EXAMPLE
&hl(sub { print shift },
[table => {border => 2, width => '80%'},
[tr =>
[td => {nowrap => &hl_bool(1)}, 'This & that'],
[td => {nowrap => &hl_bool(0)}, 'This is not bold'],
[td => [b => 'But this is']],
[td => &hl_noquote('And so is this')]]]);
prints:
This & that |
<b>This is not bold</b> |
But this is |
And so is this |
=head1 SEE ALSO
perllol(1), HTML::Tree(3)
This module was inspired by the C function in the
HTML::Tree package by Gisle Aas and Sean M. Burke.
=head1 COPYRIGHT
Copyright 2000-2002 Bob Glickstein.
This library is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.
=head1 AUTHOR
Bob Glickstein - http://www.zanshin.com/bobg/ - bobg@zanshin.com