# # 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::Util - utility functions for PXML trees =head1 SYNOPSIS =head1 DESCRIPTION =head1 NOTE This is alpha software! Read the status section in the package README or on the L. =cut package PXML::Util; use strict; use warnings; use warnings FATAL => 'uninitialized'; use Exporter "import"; our @EXPORT = qw(); our @EXPORT_OK = qw(pxml_deferred_map pxml_eager_map pxml_map_elements pxml_map_elements_exhaustively ); our %EXPORT_TAGS = (all => [@EXPORT, @EXPORT_OK]); use Chj::TEST; use FP::List; use FP::Stream ":all"; use FP::Hash "hash_perhaps_ref"; use PXML "is_pxml_element"; use Scalar::Util qw(blessed); use FP::Carp; # Mapping PXML data (works not just on elements). # Taking value first, like in OO syntax (saves a `flip` or similar # call, too). # Deferred mapping, which means that $elementfn receives the original # element with unmapped body; the body needs to be mapped or replaced # explicitely. sub pxml_deferred_map { @_ >= 2 and @_ <= 4 or fp_croak_arity "2-4"; # Elementfn receives (element, up-list, inferior-map), where # up-list is a linked list to the parents and inferior-map is a # function of one argument that will do the same as the # pxml_deferred_map call (but keep the up-list). Otherfn is # called for leafs (non-sequences) and receives (value, up-list). my ($v, $elementfn, $maybe_otherfn, $maybe_uplist) = @_; my $make_inferior_map = sub { my ($uplist) = @_; sub { pxml_deferred_map($_[0], $elementfn, $maybe_otherfn, $uplist) } }; my $uplist = $maybe_uplist // null; if (my $r = ref $v) { my $uplist2 = cons $v, $uplist; if (blessed($v) and $v->isa("PXML::Element")) { # XX TCO? &$elementfn($v, $uplist, &$make_inferior_map($uplist2)) } else { stream_map(&$make_inferior_map($uplist), stream_mixed_flatten($v)) } } else { $maybe_otherfn ? &$maybe_otherfn($v, $uplist) : $v } } # 'Eager' mapping, meaning, the body is mapped already when $elementfn # receives an element. sub pxml_eager_map { @_ >= 2 and @_ <= 4 or fp_croak_arity "2-4"; # The functions receive (value, up-list), where up-list is a # linked list to the parents my ($v, $elementfn, $maybe_otherfn, $maybe_uplist) = @_; pxml_deferred_map( $v, sub { my ($e, $uplist, $inferior_map) = @_; # XX TCO? &$elementfn($e->body_map($inferior_map), $uplist) }, $maybe_otherfn, $maybe_uplist ) } sub t_data { require PXML::XHTML; import PXML::XHTML qw(P B A CODE); P("foo", B("bar", undef, stream_iota(5)->take(4))) } TEST { t_data->string } '

foobar5678

'; TEST { pxml_eager_map( t_data, sub { $_[0]->name_update(sub { $_[0] . "A" }) }, sub { ($_[0] // "-") . "." } )->string } 'foo.bar.-.5.6.7.8.'; use FP::Ops "the_method"; sub uplist_show { my ($uplist) = @_; "[" . $uplist->map (the_method("name"))->strings_join("|") . "]" } TEST { pxml_eager_map( t_data, sub { my ($e, $uplist) = @_; $e->body_update( sub { cons(uplist_show($uplist), $_[0]) } ) }, sub { my ($v, $uplist) = @_; uplist_show($uplist) . ($v // "-") . "." } )->string } '

[][p]foo.[p][b|p]bar.[b|p]-.[b|p]5.[b|p]6.[b|p]7.[b|p]8.

'; TEST { pxml_deferred_map( t_data, sub { my ($e, $uplist, $inferior_map) = @_; $e->body_update( sub { my ($body) = @_; cons( uplist_show($uplist), $e->name eq "b" ? do { my $s = stream_mixed_flatten $body; cons(&$inferior_map(car $s), cdr $s ) } : &$inferior_map($body) ) } ); }, sub { my ($v, $uplist) = @_; uplist_show($uplist) . ($v // "-") . "." } )->string } '

[][p]foo.[p][b|p]bar.5678

'; sub pxml_map_elements { @_ >= 2 and @_ <= 3 or fp_croak_arity "2-3"; my ($v, $name_to_mapper, $maybe_otherfn) = @_; pxml_eager_map( $v, sub { my ($e, $uplist) = @_; if (my ($mapper) = hash_perhaps_ref $name_to_mapper, $e->name) { &$mapper($e, $uplist) } else { $e } }, $maybe_otherfn ); } # A variant of pxml_map_elements that applies mapper functions to # their result until there's none left or the result matches the same # function again (the latter rule is so that mapper functions can # return the same element type that they match, i.e. modify elements # instead of replacing them) # (do you have a better name?) sub pxml_map_elements_exhaustively { @_ >= 2 and @_ <= 3 or fp_croak_arity "2-3"; my ($v, $name_to_mapper, $maybe_otherfn) = @_; pxml_eager_map( $v, sub { my ($e, $uplist) = @_; LP: { my $name = $e->name; if (my ($mapper) = hash_perhaps_ref $name_to_mapper, $name) { my $v = &$mapper($e, $uplist); if (is_pxml_element $v) { $e = $v; if ($e->name eq $name) { $e } else { redo LP } } else { $v } } else { $e } } }, $maybe_otherfn ); } sub t_exh { my ($map) = @_; &$map( P(A({ href => "fun" }, "hey"), B(CODE("boo")), B("fi")), { # a mapper does not go into an endless loop: a => sub { my ($e, $uplist) = @_; $e->attribute_set(href => "foo") }, # b mapper's output, if a, still goes through the above as # well in the 'exhaustively' test; also, return value does not # need to be an element. (XX NOTE that a list around an # element, even if the element is the only list value, will # stop exhaustive processing!) b => sub { my ($e, $uplist) = @_; if (is_pxml_element stream_mixed_flatten($e->body)->first) { A({ name => "x" }, $e->body) } else { $e->body } } } )->string } TEST { t_exh \&pxml_map_elements } '

hey' . 'boo' . 'fi

'; TEST { t_exh \&pxml_map_elements_exhaustively } '

hey' . 'boo' . 'fi

'; 1