#
# Copyright (c) 2004-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

Chj::singlequote

=head1 SYNOPSIS

    use Chj::singlequote qw(singlequote singlequote_many with_maxlen);

    is with_maxlen(9, sub { singlequote "Darc's place" }),
       "'Darc\\'s...'";


=head1 DESCRIPTION

Turn strings to quoted strings.

=over 4

=item singlequote ($str, $maybe_alternative)

Perl style quoting.

If $maybe_alternative is not given, uses the string "undef" for the
undef value.

=item singlequote_sh ($str, $maybe_alternative)

Shell style quoting.

Also currently uses the "undef" value as default alternative, although
not making much sense.

=item singlequote_many (@maybe_strs)

In list context returns each argument quoted. In scalar context, join
them with a comma inbetween.

Unlike the separate ones above, this captures exceptions during the
quoting process (stringification errors) and returns "<stringification
error: $@>" in that case.

=back

=head1 NOTE

This is alpha software! Read the status section in the package README
or on the L<website|http://functional-perl.org/>.

=cut

package Chj::singlequote;
use strict;
use warnings;
use warnings FATAL => 'uninitialized';
use Exporter "import";
use FP::Carp;

our @EXPORT    = qw(singlequote);
our @EXPORT_OK = qw(singlequote_sh singlequote_many many with_maxlen
    possibly_singlequote_sh singlequote_sh_many
    quote_javascript
    quote_C _quote_C
);

# importing 'many' is probably not a good idea (depreciated)
our %EXPORT_TAGS = (all => [@EXPORT, @EXPORT_OK]);

our $maybe_maxlen;

sub with_maxlen ($&) {
    local $maybe_maxlen = $_[0];
    &{ $_[1] }()
}

# Perl style:

sub singlequote {
    @_ >= 1 and @_ <= 2 or fp_croak_arity "1-2";
    my ($str, $alternative) = @_;
    if (defined $str) {
        if (defined $maybe_maxlen and length($str) > $maybe_maxlen) {
            $str = substr($str, 0, $maybe_maxlen - 3) . "...";
        }
        $str =~ s/\'/\\\'/sg;

        # avoid newlines (and more?), try to follow the Carp::confess
        # format, if maxlen is given:
        $str =~ s/([\t\n\r])/sprintf ('\\x{%x}', ord $1)/sge
            if defined $maybe_maxlen;
        "'$str'"
    } else {
        defined($alternative) ? $alternative : "undef"
    }
}
*Chj::singlequote = \&singlequote;

sub many {
    my @strs = map {
        my $str;
        if (eval { $str = singlequote($_); 1 }) {
            $str
        } else {
            my $e = "$@";
            $e =~ s/\n.*//s;
            "<stringification error: $e>"
        }
    } @_;
    if (wantarray) {
        @strs
    } else {
        join ", ", @strs
    }
}
*singlequote_many = \&many;

# Shell (Bash) style:

sub singlequote_sh {
    @_ >= 1 and @_ <= 2 or fp_croak_arity "1-2";
    my ($str, $alternative) = @_;
    if (defined $str) {
        $str =~ s/\'/'\\\''/sg;
        "'$str'"
    } else {
        defined($alternative) ? $alternative : "undef"
    }
}
*Chj::singlequote_sh = \&singlequote_sh;

# don't quote bare words or simply formatted paths that don't need to
# be quoted
sub possibly_singlequote_sh {
    @_ == 1 or fp_croak_arity 1;
    my ($str) = @_;
    if ($str =~ m{^[=\w/.-]+\z}) {
        $str
    } else {
        singlequote_sh $str
    }
}

sub singlequote_sh_many {
    join " ", map { possibly_singlequote_sh $_ } @_
}

sub quote_javascript {
    @_ == 1 or fp_croak_arity 1;
    my ($str) = @_;

    #require JSON::MaybeXS;
    #JSON->new->allow_nonref(1)->encode($str)

    # this doesn't turn special characters into backslash sequences
    #$str =~ s|\\|\\\\|sg;
    #$str =~ s|\"|\\\"|sg;
    #'"'.$str.'"'

    # <mst> if you're obsessed with avoiding dependencies, just use
    #       JSON::PP directly and suck up the terrible performance
    require JSON::PP;
    JSON::PP->new->allow_nonref(1)->encode($str)

        # <mst> note that JSON::MaybeXS is trivial and fatpacks fine
        # <mst> intentionally
}

sub _quote_C {
    @_ == 1 or fp_croak_arity 1;
    my ($str) = @_;
    $str =~ s{(.)}{
        my $c = $1;
        my $i = ord $c;
        # https://en.wikipedia.org/wiki/Ascii
        ($i >= 32 and $i < 127) ?
          ( $c eq '"' ? "\\\"" :
            $c )
          :
          ( $c eq "\n" ? "\\n" :
            $c eq "\r" ? "\\r" :
            $c eq "\t" ? "\\t" :
            sprintf "\\%o", $i )
    }sge;
    $str
}

sub quote_C {
    @_ == 1 or fp_croak_arity 1;
    my ($str) = @_;
    '"' . _quote_C($str) . '"'
}

1
