#
# Copyright (c) 2015-2020 Christian Jaeger, copying@christianjaeger.ch
#
# This is free software, offered under either the same terms as perl 5
# or the terms of the Artistic License version 2 or the terms of the
# MIT License (Expat version). See the file COPYING.md that came
# bundled with this file.
#
=head1 NAME
PXML::Preserialize - faster PXML templating through preserialization
=head1 SYNOPSIS
use PXML::Preserialize qw(pxmlfunc pxmlpre);
use PXML::XHTML qw(A B);
my $link_normal = sub {
my ($href,$body)=@_;
A {href=> $href}, $body
};
my $link_fast = pxmlfunc {
my ($href,$body)=@_; # can take up to 10[?] arguments.
A {href=> $href}, $body
};
# the `2` is the number of arguments
my $link_fast2 = pxmlpre 2, $link_normal;
# these expressions are all returning the same result, but the second
# and third are (supposedly) evaluated faster than the first:
is $link_normal->("http://foo", [B("Foo"), "Bar"])->string,
'FooBar';
is $link_fast->("http://foo", [B("Foo"), "Bar"])->string,
'FooBar';
is $link_fast2->("http://foo", [B("Foo"), "Bar"])->string,
'FooBar';
=head1 DESCRIPTION
=for test ignore
PXML represents every XML/HTML element as an individual Perl object,
and both building up a PXML tree and serializing it is somewhat
costly.
And even if only a few strings change in a (sub)tree, a new tree
instance needs to be created and serialized for every set of those
strings.
This overhead can be eliminated by pre-serializing the segments of the
tree that don't change.
This module offers C, a function that takes a user supplied
function which maps some number of arguments to a PXML tree with those
arguments inserted, and returns a function that maps those same
arguments to an array with preserialized fragments and the (escaped)
argument values so that the PXML serialization functions don't have
any work to do except for printing the fragments and values.
With the example from the synopsis:
&$link_normal("foo","bar")
returns a PXML element with name "a", a hash C<{href=> "foo"}> as
attributes, and "bar" as the body. C<<->string>> walks over the element
and hash and body and turns all parts into the proper XML syntax.
&$link_fast2("foo","bar")
returns
bless [ $fragment1, "foo", $fragment2, "$bar", $fragment3 ], "PXML::Body"
where $fragment1 is the string ' [@EXPORT, @EXPORT_OK]);
use FP::Carp;
{
package PXML::Preserialize::Serialized;
sub new {
my ($class, $str) = @_;
bless \($str), $class
}
sub pxml_serialized_body_string {
my $s = shift;
$$s
}
}
{
package PXML::Preserialize::Argument;
use FP::Struct ["effecter", "n"], 'FP::Struct::Show', 'FP::Abstract::Pure';
# Prevent erroneous usage:
use overload (
'""' => 'err',
'0+' => 'err',
#'+' => 'err',
fallback => 1 # necessary not to have to provide + etc.
);
sub err {
die "tried to access a " . __PACKAGE__ . " object"
}
# Called when used correctly:
sub pxml_serialized_body_string {
my $self = shift;
my ($fh) = @_;
flush $fh or die $!;
$self->effecter->(0, $self->n);
""
}
sub pxml_serialized_attribute_string {
my $self = shift;
my ($fh) = @_;
flush $fh or die $!;
$self->effecter->(1, $self->n);
""
}
_END_
}
use PXML::Serialize qw(pxml_print_fragment_fast attribute_escape);
use PXML qw(pxmlbody pxmlflush);
use FP::Div qw(max);
# passes $fn $nargs arguments that it will use during serialization to
# cut apart the serialized representation.
sub _pxmlpre {
@_ == 2 or fp_croak_arity 2;
my ($nargs, $fn) = @_;
my @items;
my $buf = "";
my $lasti = 0;
my $effecter = sub {
my ($is_attribute, $n) = @_;
# let $buf grow unimpeded (setting it to "" here seems to mess
# up perl: string is regrown to previous size, but shows
# what's probably uninitialized memory, heh!)
push @items, PXML::Preserialize::Serialized->new(substr $buf, $lasti)
if $lasti < length $buf;
push @items, [$is_attribute, $n];
$lasti = length $buf;
};
my @args = map { PXML::Preserialize::Argument->new($effecter, $_) }
0 .. $nargs - 1;
my $res = &$fn(@args);
open my $out, ">", \$buf or die $!;
pxml_print_fragment_fast($res, $out);
close $out or die $!;
push @items, PXML::Preserialize::Serialized->new(substr $buf, $lasti)
if $lasti < length $buf;
\@items
}
sub build {
my ($nargs, $items) = @_;
# return interpreter(?), not compilate (to avoid eval (overhead?))
sub {
@_ == $nargs or die "expecting $nargs argument(s), got " . @_;
pxmlbody(
map {
ref($_) eq "ARRAY"
? do {
my ($is_attribute, $i) = @$_;
$is_attribute
? PXML::Preserialize::Serialized->new(
attribute_escape($_[$i]))
# otherwise let the default escaper in the
# serializer do it (this *should* always be in
# body context, XXX danger?)
: $_[$i];
}
: $_;
} @$items
)
}
}
sub pxmlpre {
@_ == 2 or fp_croak_arity 2;
my ($nargs, $fn) = @_;
build($nargs, _pxmlpre($nargs, $fn))
}
our $maxargs = 10;
sub pxmlfunc (&) {
my ($fn) = @_;
my $items = _pxmlpre($maxargs, $fn);
my $nargs
= (max(map { $$_[1] } grep { ref($_) eq "ARRAY" } @$items) // -1) + 1;
build $nargs, $items
}
1