# vim: set expandtab ts=4 sw=4 nowrap ft=perl ff=unix :
package Plack::Middleware::BetterStackTrace;
use 5.008005;
use strict;
use warnings;

our $VERSION = "0.01";

use parent qw/Plack::Middleware/;

use JSON;
use Try::Tiny;
use Data::Dumper;
use Text::Xslate;
use Plack::Request;
use Term::ANSIColor;
use Text::Xslate::Util;
use Devel::StackTrace::WithLexicals;
use Plack::Util::Accessor qw( force no_print_errors );

my $dumper = sub {
    my $value = shift;
    $value = $$value if ref $value eq 'SCALAR' or ref $value eq 'REF';
    my $d = Data::Dumper->new([$value]);
    $d->Indent(1)->Terse(1)->Deparse(1);
    chomp(my $dump = $d->Dump);
    $dump;
};

sub call {
    my ($self, $env) = @_;

    my $trace;
    local $SIG{__DIE__} = sub {
        $|     = 1;
        $trace = Devel::StackTrace::WithLexicals->new(
            indent => 1, message => munge_error($_[0], [caller]),
            ignore_package => __PACKAGE__,
        );
        die @_;
    };

    my $caught;
    my $res = try {
        $self->app->($env);
    }
    catch {
        $caught = $_;
        [
            500, [ "Content-Type", "text/plain; charset=utf-8" ],
            [ no_trace_error(utf8_safe($caught)) ]
        ];
    };

    if (
        $trace
        && ($caught
            || ($self->force && ref $res eq 'ARRAY' && $res->[0] == 500))
      ) {
        my $text = render_text(
            $trace, $env,
            application_caller_subroutine =>
              $self->{application_caller_subroutine} || '',
        );
        my $html = render_html(
            $trace, $env,
            application_caller_subroutine =>
              $self->{application_caller_subroutine} || '',
        );
        $env->{'plack.stacktrace.text'} = $text;
        $env->{'plack.stacktrace.html'} = $html;
        $env->{'psgi.errors'}->print($text) unless $self->no_print_errors;
        my $accept_mime_types = $env->{HTTP_ACCEPT} || '*/*';
        if ($accept_mime_types =~ /html/) {
            $res = [
                500, [ 'Content-Type' => 'text/html; charset=utf-8' ],
                [ utf8_safe($html) ]
            ];
        } else {
            $res = [
                500, [ 'Content-Type' => 'text/plain; charset=utf-8' ],
                [ utf8_safe($text) ]
            ];
        }
    } ## end if ($trace && ($caught || ($self...)))

    # break $trace here since $SIG{__DIE__} holds the ref to it, and
    # $trace has refs to Standalone.pm's args ($conn etc.) and
    # prevents garbage collection to be happening.
    undef $trace;

    return $res;
} ## end sub call

sub no_trace_error {
    my $msg = shift;
    chomp($msg);

    return <<EOF;
The application raised the following error:

  $msg

and the StackTrace middleware couldn't catch its stack trace, possibly because your application overrides \$SIG{__DIE__} by itself, preventing the middleware from working correctly. Remove the offending code or module that does it: known examples are CGI::Carp and Carp::Always.
EOF
}

sub munge_error {
    my ($err, $caller) = @_;
    return $err if ref $err;

    # Ugly hack to remove " at ... line ..." automatically appended by perl
    # If there's a proper way to do this, please let me know.
    $err =~ s/ at \Q$caller->[1]\E line $caller->[2]\.\n$//;

    return $err;
}

sub utf8_safe {
    my $str = shift;

    # NOTE: I know messing with utf8:: in the code is WRONG, but
    # because we're running someone else's code that we can't
    # guarnatee which encoding an exception is encoded, there's no
    # better way than doing this. The latest Devel::StackTrace::AsHTML
    # (0.08 or later) encodes high-bit chars as HTML entities, so this
    # path won't be executed.
    if (utf8::is_utf8($str)) {
        utf8::encode($str);
    }

    $str;
}

sub frame_filter {
    my ($trace, $tx, %opt) = @_;

    my @frames = $trace->frames();
    my @filtered_frames;
    my @variable_info_htmls;

    my $context = 'application';

    for my $i (0 .. $#frames) {
        my $frame = $frames[$i];
        my $next_frame = ($i == $#frames - 1) ? undef : $frames[ $i + 1 ];

        my @method_name = split('::', $frame->subroutine);
        my $method_name = pop(@method_name);
        my $module_name = join('::', @method_name);

        if ($module_name) {
            $method_name = '::' . $method_name;
        }

        if (   $next_frame
            && $next_frame->subroutine eq $opt{application_caller_subroutine}) {
            $context = 'dunno';
        }

        push @filtered_frames,
          +{
            context     => $context,
            subroutine  => $frame->subroutine,
            module_name => $module_name,
            method_name => $method_name,
            filename    => $frame->filename,
            line        => $frame->line,
          };

        my @args = $next_frame ? $next_frame->args : undef;
        push @variable_info_htmls,
          $tx->render(
            'variables_info',
            +{
                frame                     => $frame,
                args                      => \@args,
                html_formatted_code_block => context_html($frame),
            }
          );
    } ## end for my $i (0 .. $#frames)

    (\@filtered_frames, \@variable_info_htmls);
} ## end sub frame_filter

sub context_html {
    my $frame   = shift;
    my $file    = $frame->filename;
    my $linenum = $frame->line;
    my $code    = '<div class="code">';
    if (-f $file) {
        my $start = $linenum - 3;
        my $end   = $linenum + 3;
        $start = $start < 1 ? 1 : $start;
        open my $fh, '<', $file
          or die "cannot open $file:$!";
        my $cur_line = 0;
        while (my $line = <$fh>) {
            ++$cur_line;
            last if $cur_line > $end;
            next if $cur_line < $start;
            $line =~ s|\t|        |g;
            my @tag =
              $cur_line == $linenum
              ? (q{<pre class="highlight">}, '</pre>')
              : ('<pre>', '</pre>');
            $code .= sprintf(
                '%s%5d: %s%s', $tag[0], $cur_line,
                Text::Xslate::Util::html_escape($line),
                $tag[1],
            );
        }
        close $file;
    }
    $code .= '</div>';
    $code;
} ## end sub context_html

sub render_text {
    my ($trace, $psgi_env, %opt) = @_;

    my $text   = '';
    my $first  = 1;
    my @frames = $trace->frames;
    for my $frame ($trace->frames()) {
        if ($frame->subroutine eq $opt{application_caller_subroutine}) {
            last;
        }

        $text .= $frame->as_string($first, \%opt) . "\n";
        $first = 0;
    }
    $text;
}

sub render_html {
    my ($trace, $psgi_env, %opt) = @_;

    my $message = $trace->frame(0)->as_string(1);

    # Remove escape sequence
    $message = Term::ANSIColor::colorstrip($message);

    my $tx = Text::Xslate->new(
        syntax => 'TTerse',
        path   => +{
            base           => base_html(),
            variables_info => variables_info_html(),
        },
        function => {
            dump => $dumper,
            json => sub {
                my $target = shift;
                my $json   = JSON->new->ascii->encode($target);
                my $bs     = '\\';
                $json =~ s!/!${bs}/!go;
                $json =~ s!<!${bs}u003c!go;
                $json =~ s!>!${bs}u003e!go;
                $json =~ s!&!${bs}u0026!go;
                Text::Xslate::mark_raw($json);
            },
        }
    );

    my $request = Plack::Request->new($psgi_env);
    my ($backtrace_frames, $info_htmls) = frame_filter($trace, $tx, %opt);

    return $tx->render(
        'base',
        +{
            backtrace_frames => $backtrace_frames,
            parameters       => +{
                info_htmls => $info_htmls,
            },
            request => $request,
            message => $message,
        }
    );
} ## end sub render_html

sub base_html {
    <<'EOTMPL' }
<!DOCTYPE html>
<html>
<head>
    <title>[% message %][% IF request.request_uri %] at [% request.request_uri %][% END %]</title>
    <style>
    /* Basic reset */
    * {
        margin: 0;
        padding: 0;
    }

    table {
        width: 100%;
        border-collapse: collapse;
    }

    th, td {
        vertical-align: top;
        text-align: left;
    }

    textarea {
        resize: none;
    }

    body {
        font-size: 10pt;
    }

    body, td, input, textarea {
        font-family: helvetica neue, lucida grande, sans-serif;
        line-height: 1.5;
        color: #333;
        text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
    }

    html {
        background: #f0f0f5;
    }

    /* ---------------------------------------------------------------------
     * Basic layout
     * --------------------------------------------------------------------- */

    /* Small */
    @media screen and (max-width: 1100px) {
        html {
            overflow-y: scroll;
        }

        body {
            margin: 0 20px;
        }

        header.exception {
            margin: 0 -20px;
        }

        nav.sidebar {
            padding: 0;
            margin: 20px 0;
        }

        ul.frames {
            max-height: 200px;
            overflow: auto;
        }
    }

    /* Wide */
    @media screen and (min-width: 1100px) {
        header.exception {
           position: fixed;
           top: 0;
           left: 0;
           right: 0;
        }

        nav.sidebar,
        .frame_info {
            position: fixed;
            top: 95px;
            bottom: 0;

            box-sizing: border-box;

            overflow-y: auto;
            overflow-x: hidden;
        }

        nav.sidebar {
            width: 40%;
            left: 20px;
            top: 115px;
            bottom: 20px;
        }

        .frame_info {
            right: 0;
            left: 40%;

            padding: 20px;
            padding-left: 10px;
            margin-left: 30px;
        }
    }

    nav.sidebar {
        background: #d3d3da;
        border-top: solid 3px #a33;
        border-bottom: solid 3px #a33;
        border-radius: 4px;
        box-shadow: 0 0 6px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
    }

    /* ---------------------------------------------------------------------
     * Header
     * --------------------------------------------------------------------- */

    header.exception {
        padding: 18px 20px;

        height: 59px;
        min-height: 59px;

        overflow: hidden;

        background-color: #20202a;
        color: #aaa;
        text-shadow: 0 1px 0 rgba(0, 0, 0, 0.3);
        font-weight: 200;
        box-shadow: inset 0 -5px 3px -3px rgba(0, 0, 0, 0.05), inset 0 -1px 0 rgba(0, 0, 0, 0.05);

        -webkit-text-smoothing: antialiased;
    }

    /* Heading */
    header.exception h2 {
        font-weight: 200;
        font-size: 11pt;
    }

    header.exception h2,
    header.exception p {
        line-height: 1.4em;
        overflow: hidden;
        white-space: pre;
        text-overflow: ellipsis;
    }

    header.exception h2 strong {
        font-weight: 700;
        color: #d55;
    }

    header.exception p {
        font-weight: 200;
        font-size: 20pt;
        color: white;
    }

    header.exception:hover {
        height: auto;
        z-index: 2;
    }

    header.exception:hover h2,
    header.exception:hover p {
        padding-right: 20px;
        overflow-y: auto;
        word-wrap: break-word;
        height: auto;
        max-height: 7em;
    }

    @media screen and (max-width: 1100px) {
        header.exception {
            height: auto;
        }

        header.exception h2,
        header.exception p {
            padding-right: 20px;
            overflow-y: auto;
            word-wrap: break-word;
            height: auto;
            max-height: 7em;
        }
    }

    /* ---------------------------------------------------------------------
     * Navigation
     * --------------------------------------------------------------------- */

    nav.tabs {
        border-bottom: solid 1px #ddd;

        background-color: #eee;
        text-align: center;

        padding: 6px;

        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
    }

    nav.tabs a {
        display: inline-block;

        height: 22px;
        line-height: 22px;
        padding: 0 10px;

        text-decoration: none;
        font-size: 8pt;
        font-weight: bold;

        color: #999;
        text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
    }

    nav.tabs a.selected {
        color: white;
        background: rgba(0, 0, 0, 0.5);
        border-radius: 16px;
        box-shadow: 1px 1px 0 rgba(255, 255, 255, 0.1);
        text-shadow: 0 0 4px rgba(0, 0, 0, 0.4), 0 1px 0 rgba(0, 0, 0, 0.4);
    }

    /* ---------------------------------------------------------------------
     * Sidebar
     * --------------------------------------------------------------------- */

    ul.frames {
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    }

    /* Each item */
    ul.frames li {
        background-color: #f8f8f8;
        background: -webkit-linear-gradient(top, #f8f8f8 80%, #f0f0f0);
        background: -moz-linear-gradient(top, #f8f8f8 80%, #f0f0f0);
        background: linear-gradient(top, #f8f8f8 80%, #f0f0f0);
        box-shadow: inset 0 -1px 0 #e2e2e2;
        padding: 7px 20px;

        cursor: pointer;
        overflow: hidden;
    }

    ul.frames .name,
    ul.frames .location {
        overflow: hidden;
        height: 1.5em;

        white-space: nowrap;
        word-wrap: none;
        text-overflow: ellipsis;
    }

    ul.frames .method {
        color: #966;
    }

    ul.frames .location {
        font-size: 0.85em;
        font-weight: 400;
        color: #999;
    }

    ul.frames .line {
        font-weight: bold;
    }

    /* Selected frame */
    ul.frames li.selected {
        background: #38a;
        box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1), inset 0 2px 0 rgba(255, 255, 255, 0.01), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
    }

    ul.frames li.selected .name,
    ul.frames li.selected .method,
    ul.frames li.selected .location {
        color: white;
        text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
    }

    ul.frames li.selected .location {
        opacity: 0.6;
    }

    /* Iconography */
    ul.frames li {
        padding-left: 60px;
        position: relative;
    }

    ul.frames li .icon {
        display: block;
        width: 20px;
        height: 20px;
        line-height: 20px;
        border-radius: 15px;

        text-align: center;

        background: white;
        border: solid 2px #ccc;

        font-size: 9pt;
        font-weight: 200;
        font-style: normal;

        position: absolute;
        top: 14px;
        left: 20px;
    }

    ul.frames .icon.application {
        background: #808090;
        border-color: #555;
    }

    ul.frames .icon.application:before {
        content: 'A';
        color: white;
        text-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
    }

    /* Responsiveness -- flow to single-line mode */
    @media screen and (max-width: 1100px) {
        ul.frames li {
            padding-top: 6px;
            padding-bottom: 6px;
            padding-left: 36px;
            line-height: 1.3;
        }

        ul.frames li .icon {
            width: 11px;
            height: 11px;
            line-height: 11px;

            top: 7px;
            left: 10px;
            font-size: 5pt;
        }

        ul.frames .name,
        ul.frames .location {
            display: inline-block;
            line-height: 1.3;
            height: 1.3em;
        }

        ul.frames .name {
            margin-right: 10px;
        }
    }

    /* ---------------------------------------------------------------------
     * Monospace
     * --------------------------------------------------------------------- */

    pre, code, .repl input, .repl .prompt span, textarea {
        font-family: menlo, lucida console, monospace;
        font-size: 8pt;
    }

    /* ---------------------------------------------------------------------
     * Display area
     * --------------------------------------------------------------------- */

    .trace_info {
        background: #fff;
        padding: 6px;
        border-radius: 3px;
        margin-bottom: 2px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.03), 1px 1px 0 rgba(0, 0, 0, 0.05), -1px 1px 0 rgba(0, 0, 0, 0.05), 0 0 0 4px rgba(0, 0, 0, 0.04);
    }

    /* Titlebar */
    .trace_info .title {
        background: #f1f1f1;

        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
        overflow: hidden;
        padding: 6px 10px;

        border: solid 1px #ccc;
        border-bottom: 0;

        border-top-left-radius: 2px;
        border-top-right-radius: 2px;
    }

    .trace_info .title .name,
    .trace_info .title .location {
        font-size: 9pt;
        line-height: 26px;
        height: 26px;
        overflow: hidden;
    }

    .trace_info .title .location {
        float: left;
        font-weight: bold;
        font-size: 10pt;
    }

    .trace_info .title .location a {
        color:inherit;
        text-decoration:none;
        border-bottom:1px solid #aaaaaa;
    }

    .trace_info .title .location a:hover {
        border-color:#666666;
    }

    .trace_info .title .name {
        float: right;
        font-weight: 200;
    }

    .code, .console, .unavailable {
        background: #fff;
        padding: 5px;

        box-shadow: inset 3px 3px 3px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
    }

    .code {
        margin-bottom: -1px;
    }

    .code {
        padding: 10px 0;
        overflow: auto;
    }

    /* Source unavailable */
    p.unavailable {
        padding: 20px 0 40px 0;
        text-align: center;
        color: #b99;
        font-weight: bold;
    }

    p.unavailable:before {
        content: '\00d7';
        display: block;

        color: #daa;

        text-align: center;
        font-size: 40pt;
        font-weight: normal;
        margin-bottom: -10px;
    }

    @-webkit-keyframes highlight {
        0%   { background: rgba(220, 30, 30, 0.3); }
        100% { background: rgba(220, 30, 30, 0.1); }
    }
    @-moz-keyframes highlight {
        0%   { background: rgba(220, 30, 30, 0.3); }
        100% { background: rgba(220, 30, 30, 0.1); }
    }
    @keyframes highlight {
        0%   { background: rgba(220, 30, 30, 0.3); }
        100% { background: rgba(220, 30, 30, 0.1); }
    }

    .code .highlight {
        background: rgba(220, 30, 30, 0.1);
        -webkit-animation: highlight 400ms linear 1;
        -moz-animation: highlight 400ms linear 1;
        animation: highlight 400ms linear 1;
    }

    /* REPL shell */
    .console {
        padding: 0 1px 10px 1px;
        border-bottom-left-radius: 2px;
        border-bottom-right-radius: 2px;
    }

    .console pre {
        padding: 10px 10px 0 10px;
        max-height: 400px;
        overflow-x: none;
        overflow-y: auto;
        margin-bottom: -3px;
        word-wrap: break-word;
        white-space: pre-wrap;
    }

    /* .prompt > span + input */
    .console .prompt {
        display: table;
        width: 100%;
    }

    .console .prompt span,
    .console .prompt input {
        display: table-cell;
    }

    .console .prompt span {
        width: 1%;
        padding-right: 5px;
        padding-left: 10px;
    }

    .console .prompt input {
        width: 99%;
    }

    /* Input box */
    .console input,
    .console input:focus {
        outline: 0;
        border: 0;
        padding: 0;
        background: transparent;
        margin: 0;
    }

    /* Hint text */
    .hint {
        margin: 15px 0 20px 0;
        font-size: 8pt;
        color: #8080a0;
        padding-left: 20px;
    }

    .hint:before {
        content: '\25b2';
        margin-right: 5px;
        opacity: 0.5;
    }

    /* ---------------------------------------------------------------------
     * Variable infos
     * --------------------------------------------------------------------- */

    .sub {
        padding: 10px 0;
        margin: 10px 0;
    }

    .sub:before {
        content: '';
        display: block;
        width: 100%;
        height: 4px;

        border-radius: 2px;
        background: rgba(0, 150, 200, 0.05);
        box-shadow: 1px 1px 0 rgba(255, 255, 255, 0.7), inset 0 0 0 1px rgba(0, 0, 0, 0.04), inset 2px 2px 2px rgba(0, 0, 0, 0.07);
    }

    .sub h3 {
        color: #39a;
        font-size: 1.1em;
        margin: 10px 0;
        text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);

        -webkit-font-smoothing: antialiased;
    }

    .sub .inset {
        overflow-y: auto;
    }

    .sub table {
        table-layout: fixed;
    }

    .sub table td {
        border-top: dotted 1px #ddd;
        padding: 7px 1px;
    }

    .sub table td.name {
        width: 150px;

        font-weight: bold;
        font-size: 0.8em;
        padding-right: 20px;

        word-wrap: break-word;
    }

    .sub table td pre {
        max-height: 15em;
        overflow-y: auto;
    }

    .sub table td pre {
        width: 100%;

        word-wrap: break-word;
        white-space: normal;
    }

    /* "(object doesn't support inspect)" */
    .sub .unsupported {
      font-family: sans-serif;
      color: #777;
    }

    /* ---------------------------------------------------------------------
     * Scrollbar
     * --------------------------------------------------------------------- */

    nav.sidebar::-webkit-scrollbar,
    .inset pre::-webkit-scrollbar,
    .console pre::-webkit-scrollbar,
    .code::-webkit-scrollbar {
        width: 10px;
        height: 10px;
    }

    .inset pre::-webkit-scrollbar-thumb,
    .console pre::-webkit-scrollbar-thumb,
    .code::-webkit-scrollbar-thumb {
        background: #ccc;
        border-radius: 5px;
    }

    nav.sidebar::-webkit-scrollbar-thumb {
        background: rgba(0, 0, 0, 0.0);
        border-radius: 5px;
    }

    nav.sidebar:hover::-webkit-scrollbar-thumb {
        background-color: #999;
        background: -webkit-linear-gradient(left, #aaa, #999);
    }

    .console pre:hover::-webkit-scrollbar-thumb,
    .inset pre:hover::-webkit-scrollbar-thumb,
    .code:hover::-webkit-scrollbar-thumb {
        background: #888;
    }
    </style>

    [%# IE8 compatibility crap %]
    <script>
    (function() {
        var elements = ["section", "nav", "header", "footer", "audio"];
        for (var i = 0; i < elements.length; i++) {
            document.createElement(elements[i]);
        }
    })();
    </script>

    <script>
      if (window.Turbolinks) {
          for(var i=0; i < document.styleSheets.length; i++) {
              if(document.styleSheets[i].href)
                  document.styleSheets[i].disabled = true;
          }
          document.addEventListener("page:restore", function restoreCSS(e) {
              for(var i=0; i < document.styleSheets.length; i++) {
                  document.styleSheets[i].disabled = false;
              }
              document.removeEventListener("page:restore", restoreCSS, false);
          });
      }
    </script>
</head>
<body>
    <div class="top">
        <header class="exception">
            <h2><strong>Exception Occurs</strong>[% IF request.request_uri %] <span>at [% request.request_uri %]</span>[% END %]</h2>
            <p>[% message %]</p>
        </header>
    </div>

    <section class="backtrace">
        <nav class="sidebar">
            <nav class="tabs">
                <a href="#" id="application_frames">Application Frames</a>
                <a href="#" id="all_frames">All Frames</a>
            </nav>
            <ul class="frames">
                [% FOREACH frame IN backtrace_frames %]
                    <li class="[% frame.context %]" data-context="[% frame.context %]" data-index="[% loop.index %]">
                        <span class='stroke'></span>
                        <i class="icon [% frame.context %]"></i>
                        <div class="info">
                            <div class="name">
                                [% next_frame = loop.peek_next %]
                                [% IF next_frame AND next_frame.subroutine %]
                                    <strong>[% next_frame.module_name %]</strong><span class='method'>[% next_frame.method_name %]</span>
                                [% END %]
                            </div>
                            <div class="location">
                                <span class="filename">[% frame.filename || '(no file)' %]</span>[% IF frame.line %], line <span class="line">[% frame.line %]</span>[% END %]
                            </div>
                        </div>
                    </li>
                [% END %]
            </ul>
        </nav>

        [% FOREACH frame IN backtrace_frames %]
            <div class="frame_info" id="frame_info_[% loop.index %]" style="display:none;"></div>
        [% END %]
    </section>
</body>
<script>
(function() {
    var parameters = [% parameters | json %];

    var previousFrame = null;
    var previousFrameInfo = null;
    var allFrames = document.querySelectorAll("ul.frames li");
    var allFrameInfos = document.querySelectorAll(".frame_info");

    function apiCall(method, opts, cb) {
        // TODO: implement it
        return;
        var OID = '';
        var req = new XMLHttpRequest();
        req.open("POST", "/__better_errors/" + OID + "/" + method, true);
        req.setRequestHeader("Content-Type", "application/json");
        req.send(JSON.stringify(opts));
        req.onreadystatechange = function() {
            if(req.readyState == 4) {
                var res = JSON.parse(req.responseText);
                cb(res);
            }
        };
    }

    function escapeHTML(html) {
        return html.replace(/&/, "&amp;").replace(/</g, "&lt;");
    }

    function REPL(index) {
        this.index = index;

        this.previousCommands = [];
        this.previousCommandOffset = 0;
    }

    REPL.all = [];

    REPL.prototype.install = function(containerElement) {
        this.container = containerElement;

        this.promptElement  = this.container.querySelector(".prompt span");
        this.inputElement   = this.container.querySelector("input");
        this.outputElement  = this.container.querySelector("pre");

        this.inputElement.onkeydown = this.onKeyDown.bind(this);

        this.setPrompt(">>");

        REPL.all[this.index] = this;
    }

    REPL.prototype.focus = function() {
        this.inputElement.focus();
    };

    REPL.prototype.setPrompt = function(prompt) {
        this._prompt = prompt;
        this.promptElement.innerHTML = escapeHTML(prompt);
    };

    REPL.prototype.getInput = function() {
        return this.inputElement.value;
    };

    REPL.prototype.setInput = function(text) {
        this.inputElement.value = text;

        if(this.inputElement.setSelectionRange) {
            // set cursor to end of input
            this.inputElement.setSelectionRange(text.length, text.length);
        }
    };

    REPL.prototype.writeRawOutput = function(output) {
        this.outputElement.innerHTML += output;
        this.outputElement.scrollTop = this.outputElement.scrollHeight;
    };

    REPL.prototype.writeOutput = function(output) {
        this.writeRawOutput(escapeHTML(output));
    };

    REPL.prototype.sendInput = function(line) {
        var self = this;
        apiCall("eval", { "index": this.index, source: line }, function(response) {
            if(response.error) {
                self.writeOutput(response.error + "\n");
            }
            self.writeOutput(self._prompt + " ");
            self.writeRawOutput(response.highlighted_input + "\n");
            self.writeOutput(response.result);
            self.setPrompt(response.prompt);
        });
    };

    REPL.prototype.onEnterKey = function() {
        var text = this.getInput();
        if(text != "" && text !== undefined) {
            this.previousCommandOffset = this.previousCommands.push(text);
        }
        this.setInput("");
        this.sendInput(text);
    };

    REPL.prototype.onNavigateHistory = function(direction) {
        this.previousCommandOffset += direction;

        if(this.previousCommandOffset < 0) {
            this.previousCommandOffset = -1;
            this.setInput("");
            return;
        }

        if(this.previousCommandOffset >= this.previousCommands.length) {
            this.previousCommandOffset = this.previousCommands.length;
            this.setInput("");
            return;
        }

        this.setInput(this.previousCommands[this.previousCommandOffset]);
    };

    REPL.prototype.onKeyDown = function(ev) {
        if(ev.keyCode == 13) {
            this.onEnterKey();
        } else if(ev.keyCode == 38) {
            // the user pressed the up arrow.
            this.onNavigateHistory(-1);
            return false;
        } else if(ev.keyCode == 40) {
            // the user pressed the down arrow.
            this.onNavigateHistory(1);
            return false;
        }
    };

    function switchTo(el) {
        if(previousFrameInfo) previousFrameInfo.style.display = "none";
        previousFrameInfo = el;

        el.style.display = "block";

        var replInput = el.querySelector('.console input');
        if (replInput) replInput.focus();
    }

    function selectFrameInfo(index) {
        var el = allFrameInfos[index];
        if(el) {
            if (el.loaded) {
                return switchTo(el);
            }

            el.loaded = true;
            el.innerHTML = parameters.info_htmls[index];

            /*
            var repl = el.querySelector(".repl .console");
            if(repl) {
                new REPL(index).install(repl);
            }
            */

            switchTo(el);
        }
    }

    for(var i = 0; i < allFrames.length; i++) {
        (function(i, el) {
            var el = allFrames[i];
            el.onclick = function() {
                if(previousFrame) {
                    previousFrame.className = "";
                }
                el.className = "selected";
                previousFrame = el;

                selectFrameInfo(el.attributes["data-index"].value);
            };
        })(i);
    }

    // Click the first application frame
    (
      document.querySelector(".frames li.application") ||
      document.querySelector(".frames li")
    ).onclick();

    var applicationFramesButton = document.getElementById("application_frames");
    var allFramesButton = document.getElementById("all_frames");

    applicationFramesButton.onclick = function() {
        allFramesButton.className = "";
        applicationFramesButton.className = "selected";
        for(var i = 0; i < allFrames.length; i++) {
            if(allFrames[i].attributes["data-context"].value == "application") {
                allFrames[i].style.display = "block";
            } else {
                allFrames[i].style.display = "none";
            }
        }
        return false;
    };

    allFramesButton.onclick = function() {
        applicationFramesButton.className = "";
        allFramesButton.className = "selected";
        for(var i = 0; i < allFrames.length; i++) {
            allFrames[i].style.display = "block";
        }
        return false;
    };

    applicationFramesButton.onclick();
})();
</script>
</html>
EOTMPL

sub variables_info_html {
    <<'EOTMPL' }
<header class="trace_info">
    <div class="title">
        <h2 class="name">[% frame.subroutine %]</h2>
        <div class="location"><span class="filename">[% frame.filename %]</span></div>
    </div>

    [% html_formatted_code_block | raw %]

    <!--div class="repl">
        <div class="console">
            <pre></pre>
            <div class="prompt"><span>&gt;&gt;</span> <input/></div>
        </div>
    </div-->
</header>

<!--div class="hint">
    TODO: Live Shell (REPL) is not implemented yet.
</div-->

<div class="variable_info"></div>

[% IF frame.lexicals %]
<div class="sub">
    <h3>Lexicals</h3>
    <div class='inset variables'>
        <table class="var_table">
            [% FOREACH key IN frame.lexicals.keys() %]
                <tr><td class="name">[% key %]</td><td><pre>[% frame.lexicals.$key | dump %]</pre></td></tr>
            [% end %]
        </table>
    </div>
</div>
[% END %]

[% IF args %]
<div class="sub">
    <h3>Args</h3>
    <div class='inset variables'>
        <table class="var_table">
            [% FOREACH arg IN args %]
                <tr><td class="name">$_[[% loop.index %]]</td><td><pre>[% arg | dump %]</pre></td></tr>
            [% end %]
        </table>
    </div>
</div>
[% END %]
EOTMPL

1;
__END__

=encoding utf-8

=head1 NAME

Plack::Middleware::BetterStackTrace - Displays better stack trace when your app dies

=head1 SYNOPSIS

  enable 'BetterStackTrace',
      application_caller_subroutine => 'Amon2::Web::handle_request';

=head1 DESCRIPTION

This middleware catches exceptions (run-time errors) happening in your
application and displays nice stack trace screen. The stack trace is
also stored in the environment as a plaintext and HTML under the key
C<plack.stacktrace.text> and C<plack.stacktrace.html> respectively, so
that middleware futher up the stack can reference it.

You're recommended to use this middleware during the development and
use L<Plack::Middleware::HTTPExceptions> in the deployment mode as a
replacement, so that all the exceptions thrown from your application
still get caught and rendered as a 500 error response, rather than
crashing the web server.

Catching errors in streaming response is not supported.

This module is based on L<Plack::Middleware::StackTrace> and Better Errors for Ruby L<https://github.com/charliesome/better_errors>.

=head1 LICENSE

Perl

Copyright (C) Tasuku SUENAGA a.k.a. gunyarakun.

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

HTML/CSS/JavaScript

Copyright (C) 2012 Charlie Somerville

MIT License

=head1 AUTHOR

Tasuku SUENAGA a.k.a. gunyarakun E<lt>tasuku-s-github@titech.acE<gt>

=head1 TODO

- REPL
- JSON response

=head1 SEE ALSO

L<Plack::Middleware::StackTrace> L<Devel::StackTrace::AsHTML> L<Plack::Middleware> L<Plack::Middleware::HTTPExceptions>

=cut
