{ my %coll; $coll{contents} = \0; # A ref. Any ref. my $element = sub { my ($name, $optional_newline) = @_;; $optional_newline ||= ''; return sub (&) { my $ret = do { # I cannot localize lexicals, but I can localize members of a # lexically scoped aggregate, much to the same effect: local $coll{contents}=""; local $coll{attributes}=""; shift->(); "<$name$coll{attributes}>$optional_newline" . "$coll{contents}$optional_newline"; }; if (defined wantarray) { return $ret; } elsif (ref $coll{contents}) { return; } else { $coll{contents} .= $ret; return; # We are in void context, but might as well be explicit. } }; }; sub table (&); sub row (&); sub cell (&); *table = $element->( table => $/ ); *row = $element->( 'tr' => $/ ); *cell = $element->( 'td' ); # The only way to get any actual contents: sub lit (@) { $coll{contents} .= $_ for @_ } # In the simple case, this one reads a lot better: sub cells (@) { cell { lit $_; } for @_ } # I cannot approach a templating system without attributes, right? # (I am sorry already. I just cannot help it ...) sub att (@) { # Sets/unsets all attributes each time -- last call gets served: $coll{attributes}=""; while (@_) { my ($key, $value) = (shift, shift); unless (defined $value) { $coll{attributes} .= " $key"; next; } # From CGI::Utils::simple_escape: $value =~ s{&}{&}gso; $value =~ s{<}{<}gso; $value =~ s{>}{>}gso; $value =~ s{\"}{"}gso; $coll{attributes} .= qq( $key="$value"); } } } # Your examples would then need s/cell/cells/g and a print: print table { for (1..10) { row { cells "foo", "bar", "baz"; } } }; print table { row { cells @$_ } for $db->query('SELECT * FROM foo')->arrays }; # I have been known to overdesign my examples as well ... print table { att border => 1; my %valign = qw( foo top bar center baz bottom); my $quux_cell = sub { my $quux = shift; cell { lit $quux; att valign => $valign{$quux} if $valign{$quux}; }; }; for (1..10) { row { cell { table { for ("top", "center", "bottom") { row { cell { att align => 'center'; lit $_; }; }; } }; }; for (qw/foo bar baz/) { $quux_cell->($_); } } } }; # New examples, per the update (see above): # Would have had the rows appearing twice in my original implementation; # the first time in the order generated: print table { my @r = map {row { cells $_, $_*$_ }} 1..10; for (0..$#r) { my $j = rand($_); @r[$_,$j]=@r[$j,$_]; } lit @r; }; # Would have had all of the inner tables appearing twice in my # original implementation; the first time at the start of the row, # outside of the TD elements: print table { my $x; for (1..3) { row { for (1..3) { ++$x; cells $x, table { att border => 1; for (1..$x) { row { cells((" ")x$x) } } }; } }; } };