<commit_before>package MarpaX::Text::Caml;

use strict;
use warnings;

use feature 'say';

require Scalar::Util;
use Clone qw(clone);
use List::Util qw(any);
use Marpa::R2;
use Data::Dumper::OneLine;

our $VERSION = '0.1';

sub dumper {
    require Data::Dumper;

    return Data::Dumper::Dumper @_;
}

sub new {
    my $class = shift;

    return bless {}, $class;
}

sub render {
    my $self = shift;
    my $template = shift;
    my $data = shift;

    return '' unless $template;

    return $self->_compile($template)->($data);
}

sub _compile {
    my $self = shift;
    my $template = shift;
    my $parsed = $self->_parse($template);

    # say dumper $parsed;

    my $code = eval $parsed // die $@;

    return sub {
        my $input = shift;

        $input = $self->_expand_variables($input);

        return $code->($input, \&_section);
    };
}

sub _expand_variables {
    my $self = shift;
    my $input = shift // {};

    foreach my $key ( keys $input ) {
        my $value = $input->{$key};

        if ( ref $value eq 'HASH' && !Scalar::Util::blessed($value) ) {
            $input->{$key} = $self->_expand_variables($value);
        }
        elsif ( ref $value eq 'ARRAY' ) {
            if ( any { ref $_ ne 'HASH' } @$value ) {
                $input->{$key} = [ map { { _self => $_ } } @$value ];
            }
            else {
                $input->{$key} = [ map { $self->_expand_variables($_) } @$value ];
            }

            $input->{$key} = $self->_add_indexes($input->{$key});
        }
    }

    return $input;
}

sub _add_indexes {
    my $self = shift;
    my $value = shift;

    my $i = -1;
    return [
        map {
            $i++;

            {
                _idx    => $i,
                _first  => $i == 0 ? 1 : 0,
                _last   => $i == scalar(@$value) - 1 ? 1 : 0,
                _even   => $i % 2 == 0 ? 1 : 0,
                _odd    => $i % 2 == 0 ? 0 : 1,
                %$_,
            }
        } @$value
    ];
}

sub _find_key_in_parent_scope {
    my $input = shift;
    my @keys = @_;
    my $last = pop @keys;

    pop @keys; # remove the containing scope

    return _find_key($input, @keys, $last);
}

sub _find_key {
    my $input = shift;
    my @keys = @_;
    my $value = clone($input);

    # Walk down the context to find the value
    foreach my $key ( @keys ) {
        # say dumper [ @keys ], $value;

        if ( Scalar::Util::blessed($value) ) {
            $value = $value->$key
        }
        elsif ( ref $value eq 'ARRAY' ) {
            if ( Scalar::Util::looks_like_number($key) ) {
                $value = $value->[$key];
            }
            else {
                return _find_key_in_parent_scope($input, @keys);
            }
        }
        elsif ( ref $value eq 'HASH' ) {
            $value = $value->{$key};
        }
        elsif ( scalar @keys > 1 ) {
            return _find_key_in_parent_scope($input, @keys);
        }
        else {
            return undef;
        }
    }

    if ( !defined $value && scalar @keys > 1 ) {
        return _find_key_in_parent_scope($input, @keys);
    }

    return $value;
}

sub _section {
    my $input = shift;
    my $ra_context = shift;
    my @children = @_;

    my $value = _find_key($input, @$ra_context);
    return '' unless $value;

    if ( ref $value eq 'ARRAY' ) {
        return map { _section($input, [ @$ra_context, $_->{_idx} ], @children) } @$value;
    }

    @children = _interpolate_variables($input, $ra_context, @children);
    @children = _remove_whitespace($input, $ra_context, @children);

    return join '', @children;
};

sub _remove_whitespace {
    my $input = shift;
    my $ra_context = shift;
    my @children = @_;

    return grep { defined && m/\S/ } @children;
}

sub _interpolate_variables {
    my $input = shift;
    my $ra_context = shift;
    my @children = @_;

    return map {
        if ( ref $_ eq 'HASH' && $_->{type} eq 'variable' ) {
            my $value = _find_key($input, @$ra_context, $_->{value});

            $_->{escape} ? _escape($value) : $value;
        }
        else {
            $_;
        }
    } @children;
}

sub _escape {
    my $value = shift;<commit_msg>Get odd even checks to work<commit_after>package MarpaX::Text::Caml;

use strict;
use warnings;

use feature 'say';

require Scalar::Util;
use Clone qw(clone);
use List::Util qw(any none);
use Marpa::R2;
use Data::Dumper::OneLine;

our $VERSION = '0.1';

sub dumper {
    require Data::Dumper;

    return Data::Dumper::Dumper @_;
}

sub new {
    my $class = shift;

    return bless {}, $class;
}

sub render {
    my $self = shift;
    my $template = shift;
    my $data = shift;

    return '' unless $template;

    return $self->_compile($template)->($data);
}

sub _compile {
    my $self = shift;
    my $template = shift;
    my $parsed = $self->_parse($template);

    # say dumper $parsed;

    my $code = eval $parsed // die $@;

    return sub {
        my $input = shift;

        $input = $self->_expand_variables($input);

        return $code->($input, \&_section);
    };
}

sub _expand_variables {
    my $self = shift;
    my $input = shift // {};

    foreach my $key ( keys $input ) {
        my $value = $input->{$key};

        if ( ref $value eq 'HASH' && !Scalar::Util::blessed($value) ) {
            $input->{$key} = $self->_expand_variables($value);
        }
        elsif ( ref $value eq 'ARRAY' ) {
            if ( any { ref $_ ne 'HASH' } @$value ) {
                $input->{$key} = [ map { { _self => $_ } } @$value ];
            }
            else {
                $input->{$key} = [ map { $self->_expand_variables($_) } @$value ];
            }

            $input->{$key} = $self->_add_indexes($input->{$key});
        }
    }

    return $input;
}

sub _add_indexes {
    my $self = shift;
    my $value = shift;

    my $i = -1;
    return [
        map {
            $i++;

            {
                _idx    => $i,
                _first  => $i == 0 ? 1 : 0,
                _last   => $i == scalar(@$value) - 1 ? 1 : 0,
                _even   => $i % 2 == 0 ? 1 : 0,
                _odd    => $i % 2 == 0 ? 0 : 1,
                %$_,
            }
        } @$value
    ];
}

sub _find_key_in_parent_scope {
    my $input = shift;
    my @keys = @_;
    my $last = pop @keys;

    pop @keys; # remove the containing scope

    return _find_key($input, @keys, $last);
}

sub _find_key {
    my $input = shift;
    my @keys = @_;
    my $value = clone($input);

    # Walk down the context to find the value
    foreach my $key ( @keys ) {
        # say dumper [ @keys ], $value;

        if ( Scalar::Util::blessed($value) ) {
            $value = $value->$key
        }
        elsif ( ref $value eq 'ARRAY' ) {
            if ( Scalar::Util::looks_like_number($key) ) {
                $value = $value->[$key];
            }
            else {
                return _find_key_in_parent_scope($input, @keys);
            }
        }
        elsif ( ref $value eq 'HASH' ) {
            $value = $value->{$key};
        }
        elsif ( scalar @keys > 1 ) {
            return _find_key_in_parent_scope($input, @keys);
        }
        else {
            return undef;
        }
    }

    if ( !defined $value && scalar @keys > 1 ) {
        return _find_key_in_parent_scope($input, @keys);
    }

    return $value;
}

sub _section {
    my $input = shift;
    my $ra_context = shift;
    my @children = @_;

    # unless this is the base node use _process_sections
    return _process_sections($input, $ra_context, @children) if scalar(@$ra_context);

    my @elements = _process_sections($input, $ra_context, @children);

    return join '', @elements;
}

sub _process_sections {
    my $input = shift;
    my $ra_context = shift;
    my @children = @_;

    my $value = _find_key($input, @$ra_context);

    if ( none { Scalar::Util::looks_like_number($_) } @$ra_context ) {
        my @scope = @$ra_context;
        my @rest;
        while ( @scope ) {
            my $current = shift @scope;
            my $next = $scope[0];

            push @rest, $current;
            my $value = _find_key($input, @rest);


            if ( ref $value eq 'ARRAY' ) {
                if ( @scope ) {
                    return sub {
                        my $index = shift;

                        _process_sections($input, [@rest, $index, @scope], @children)
                    };
                }
                else {
                    return map {
                        my $index = $_->{_idx};

                        my @inner = map { ref $_ eq 'CODE' ? $_->($index) : $_ } @children;

                        _process_sections($input, [ @rest, $index ], @inner);
                    } @$value;
                }

            }
        }
    }

    return () unless $value;
    @children = _interpolate_variables($input, $ra_context, @children);
    @children = _remove_whitespace($input, $ra_context, @children);

    return @children;
};

sub _remove_whitespace {
    my $input = shift;
    my $ra_context = shift;
    my @children = @_;

    return grep { defined && m/\S/ } @children;
}

sub _interpolate_variables {
    my $input = shift;
    my $ra_context = shift;
    my @children = @_;

    return map {
        if ( ref $_ eq 'HASH' && $_->{type} eq 'variable' ) {
            my $value = _find_key($input, @$ra_context, $_->{value});

            $_->{escape} ? _escape($value) : $value;
        }
        else {
            $_;
        }
    } @children;
}

sub _escape {
    my $value = shift;
{'fraction_non_alphanumeric': 0.15383007628801337, 'fraction_numerical': 0.004284669244435155, 'mean_word_length': 1.7095130237825595, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 62, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2190898', 'n_tokens_mistral': 3223, 'n_tokens_neox': 3093, 'n_words': 789}