package Template::Caribou::Tags;
our $AUTHORITY = 'cpan:YANICK';
#ABSTRACT: generates tags functions for Caribou templates
$Template::Caribou::Tags::VERSION = '1.2.2';
use strict;
use warnings;

use Carp;

use Template::Caribou::Role;

use List::AllUtils qw/ pairmap pairgrep /;
use Ref::Util qw/ is_plain_hashref /;

use parent 'Exporter::Tiny';
use experimental 'signatures', 'postderef';
use XML::Writer;

our @EXPORT_OK = qw/ render_tag mytag attr /;


sub attr(@args){
    return $_{$args[0]} if @args == 1;

    croak "number of attributes must be even" if @args % 2;

    no warnings 'uninitialized';
    while( my ( $k, $v ) = splice @args, 0, 2 ) {
        if ( $k =~ s/^\+// ) {
            $_{$k} = { map { $_ => 1 } split ' ', $_{$k} }
                unless ref $_{$k};

            $_{$k}{$v} = 1;
        }
        elsif ( $k =~ s/^-// ) {
            $_{$k} = { map { $_ => 1 } split ' ', $_{$k} }
                unless ref $_{$k};

            delete $_{$k}{$v};
        }
        else {
            $_{$k} = $v;
        }
    }

    return;
}


sub _generate_mytag {
    my ( undef, undef, $arg ) = @_;

    $arg->{'-as'} ||= $arg->{tag}
        or die "mytag needs to be given '-as' or 'name'\n";

    my $tagname = $arg->{tag} || 'div';

    my $groom = sub {
        
        no warnings 'uninitialized';

        if( my $defaults = $arg->{classes} || $arg->{class} ) {
            $_{class} = { map { $_ => 1 } split ' ', $_{class} }
                unless ref $_{class};
            if( ref $defaults ) {
                $_{class}{$_} //= 1 for @$defaults;
            }
            else {
                $_{class}{$_} //=  1 for split ' ', $defaults;
            }
        }

        $_{$_} ||= $arg->{attr}{$_} for eval { keys %{ $arg->{attr} } };

        $arg->{groom}->() if $arg->{groom};
    };

    return sub :prototype(&) {
        my $inner = shift;
        render_tag( $tagname, $inner, $groom, $arg->{indent}//1 );
    }
}


sub render_tag {
    my ( $tag, $inner_sub, $groom, $indent ) = @_;

    $indent //= 1;

    local $Template::Caribou::TAG_INDENT_LEVEL = $indent ? $Template::Caribou::TAG_INDENT_LEVEL : 0;

    my $sub = ref $inner_sub eq 'CODE' ? $inner_sub : sub { $inner_sub };

    # need to use the object for calls to 'show'
    my $bou = $Template::Caribou::TEMPLATE || Moose::Meta::Class->create_anon_class(
        roles => [ 'Template::Caribou::Role' ]
    )->new_object;

    local %_;

    my $inner = do {
        local $Template::Caribou::TAG_INDENT_LEVEL = $Template::Caribou::TAG_INDENT_LEVEL;

        $Template::Caribou::TAG_INDENT_LEVEL++
            if $Template::Caribou::TAG_INDENT_LEVEL // $bou->indent;

        $bou->get_render($sub);
    };

    if ( $groom ) {
        local $_ = "$inner";  # stringification required in case it's an object

        $groom->();

        $inner = $_;
    }

    # Setting UNSAFE here so that the inner can be written with raw
    # as we don't want inner to be escaped as it is already escaped
    my $writer = XML::Writer->new(OUTPUT => 'self', UNSAFE => 1);
    my @attributes = pairmap { (  $a => $b ) x (length $b > 0) }
        map { 
            $_ => is_plain_hashref($_{$_})
                ? join ' ', sort { $a cmp $b } pairmap { $a } pairgrep { $b } $_{$_}->%* 
                : $_{$_} 
        }
       grep { defined $_{$_} }
       sort keys %_;

    no warnings qw/ uninitialized /;

    my $prefix = !!$Template::Caribou::TAG_INDENT_LEVEL
        && "\n" . ( '  ' x $Template::Caribou::TAG_INDENT_LEVEL );

    if (length($inner)) {
        $writer->startTag($tag, @attributes);
        $writer->raw("$inner$prefix");
        $writer->endTag($tag);
    }
    else {
        $writer->emptyTag($tag, @attributes);
    }

    my $output = Template::Caribou::String->new( $prefix . $writer->to_string() );

    return print_raw( $output );
}

sub print_raw($text) {
    print ::RAW $text;
    return $text;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Template::Caribou::Tags - generates tags functions for Caribou templates

=head1 VERSION

version 1.2.2

=head1 SYNOPSIS

    package MyTemplate;

    use Template::Caribou;

    use Template::Caribou::Tags
        mytag => { 
            -as   => 'foo',
            tag   => 'p',
            class => 'baz'
        };

    template bar => sub {
        foo { 'hello' };
    };

    # <p class="baz">hello</p>
    print __PACKAGE__->new->bar;

=head1 DESCRIPTION

This module provides the tools to create tag libraries, or ad-hoc tags.

For pre-defined sets of tags, you may want to look at L<Template::Caribou::Tags::HTML>,
L<Template::Caribou::Tags::HTML::Extended>, and friends.

=head2 Core functionality

Tag functions are created using the C<render_tag> function. For example:

    package MyTemplate;

    use Template::Caribou;

    use Template::Caribou::Tags qw/ render_tag /;

    sub foo(&) { render_tag( 'foo', shift ) }

    # renders as '<foo>hi!</foo>'
    template main => sub {
        foo { "hi!" };
    };   

=head2 Creating ad-hoc tags

Defining a function and using C<render_tag> is a little bulky and, typically, will only be used when creating
tag libraries. In most cases, 
the C<my_tag> export keyword can be used to create custom tags. For example, the
previous C<foo> definition could have been done this way:

    package MyTemplate;

    use Template::Caribou;

    use Template::Caribou::Tags
        mytag => { tag => 'foo' };

    # renders as '<foo>hi!</foo>'
    template main => sub {
        foo { 
            "hi!";
        };
    };   

=head1 EXPORTS

By default, nothing is exported.
The functions C<render_tag> and C<attr> can be exported by this module. 

Custom tag functions can also be defined via the export keyword C<mytag>.

C<mytag> accepts the following arguments:

=over

=item tag => $name

Tagname that will be used. If not specified, defaults to C<div>.

=item -as => $name

Name under which the tag function will be exported. If not specified, defaults to the 
value of the C<tag> argument. At least one of C<-as> or C<tag> must be given explicitly.

=item groom => sub { }

Grooming function for the tag block. See C<render_tag> for more details.

=item classes => \@classes

Default value for the 'class' attribute of the tag. 

    use Template::Caribou::Tags 
                    # <div class="main">...</div>
        mytag => { -as => 'main_div', classes => [ 'main' ] };

If you want to remove a default class from the tag,
set its value to C<0> in C<%_>. E.g.,

    main_div { $_{class}{main} = 0; ... };

=item attr => \%attributes

Default set of attributes for the tag.

    use Template::Caribou::Tags 
                    # <input disabled="disabled">...</input>
        mytag => { -as => 'disabled_input', tag => 'input', attr => { disabled => 'disabled' } };

=back

=function attr( $name => $value )

I recommend you use C<%_> directly instead.

Accesses the attributes of a tag within its block.

If provided an even number of parameters, sets the attributes to those values.

    div {
        attr class => 'foo', 
             style => 'text-align: center';

        "hi there";
    };

    # <div class="foo" style="text-align: center">hi there</div>

Many calls to C<attr> can be done within the same block.

    div {
        attr class => 'foo';
        attr style => 'text-align: center';

        "hi there";
    };

    # <div class="foo" style="text-align: center">hi there</div>

To add/remove to an attribute instead of replacing its value, prefix the attribute name
with a plus or minus sign. Doing either will automatically 
turn the value in C<%_> to a hashref.

    div {
        attr class    => 'foo baz';

        attr '+class' => 'bar';
        attr '-class' => 'baz';

        "hi there";
    };

    # <div class="foo bar">hi there</div>

The value of an attribute can also be queried by passing a single argument to C<attr>.

    div { 
        ...; # some complex stuff here

        my $class = attr 'class';

        attr '+style' => 'text-align: center' if $class =~ /_centered/;

        ...;
    }

=function render_tag( $tag_name, $inner_block, \&groom, $indent )

Prints out a tag in a template. The C<$inner_block> is a string or coderef
holding the content of the tag. 

If the C<$inner_block> is empty, the tag will be of the form
C<< <foo /> >>.

    render_tag( 'div', 'hello' );         #  <div>hello</div>

    render_tag( 'div', sub { 'hello' } )  # <div>hello</div>

    render_tag( 'div', '' );              #  <div />

An optional grooming function can be passed. If it is, an hash holding the 
attributes of the tag, and its inner content will be passed to it as C<%_> and C<$_>, respectively.

   # '<div>the current time is Wed Nov 25 13:18:33 2015</div>'
   render_tag( 'div', 'the current time is DATETIME', sub {
        s/DATETIME/scalar localtime/eg;
   });

   # '<div class="mine">foo</div>'
   render_tag( 'div', 'foo', sub { $_{class} = 'mine' } )

An optional C<$indent> argument can also be given. If explicitly set to
C<false>, the tag won't be indented even when the template
is in pretty-print mode. Used for tags where whitespaces
are significant or would alter
the presentation (e.g., C<pre> or C<emphasis>). Defaults to C<true>.

=head1 AUTHOR

Yanick Champoux <yanick@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2023 by Yanick Champoux.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut