# # Copyright (c) 2015 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 package README. =cut package PXML::Util; @ISA="Exporter"; require Exporter; @EXPORT=qw(); @EXPORT_OK=qw(pxml_deferred_map pxml_eager_map pxml_map_elements pxml_map_elements_exhaustively ); %EXPORT_TAGS=(all=>[@EXPORT,@EXPORT_OK]); use strict; use warnings; use warnings FATAL => 'uninitialized'; use Chj::TEST; use FP::List; use FP::Stream ":all"; use FP::Hash "hash_perhaps_ref"; use PXML "is_pxml_element"; # 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 ($$;$$) { # 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 (UNIVERSAL::isa ($v, "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 ($$;$$) { # 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 } '[][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 ($$;$) { 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 ($$;$) { 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 } ''; TEST{ t_exh \&pxml_map_elements_exhaustively } ''; 1