package ES::Util;

use strict;
use warnings;
use v5.10;

use File::Copy::Recursive qw(rcopy rmove);
use Capture::Tiny qw(capture tee);
use Encode qw(decode_utf8);
use Path::Class qw(dir file);
use Parallel::ForkManager();

binmode( STDOUT, ':encoding(utf8)' );

require Exporter;
our @ISA       = qw(Exporter);
our @EXPORT_OK = qw(
    run $Opts
    build_chunked build_single
    proc_man
    timestamp
    write_html_redirect
    write_nginx_redirects
    write_nginx_test_config
    write_nginx_preview_config
    start_web_resources_watcher
    start_preview
    build_web_resources
);

our $Opts = { procs => 3, lang => 'en' };

#===================================
sub build_chunked {
#===================================
    my ( $index, $raw_dest, $dest, %opts ) = @_;

    my $single    = 0;
    my $chunk     = $opts{chunk}         || 0;
    my $version   = $opts{version}       || '';
    my $multi     = $opts{multi}         || 0;
    my $lenient   = $opts{lenient}       || '';
    my $lang      = $opts{lang}          || 'en';
    my $edit_urls = $opts{edit_urls};
    my $section   = $opts{section_title} || '';
    my $subject   = $opts{subject}       || '';
    my $private   = $opts{private}       || '';
    my $resources = $opts{resource}      || [];
    my $noindex   = $opts{noindex}       || '';
    my $page_header = custom_header($index) || $opts{page_header} || '';
    my $latest    = $opts{latest};
    my $respect_edit_url_overrides = $opts{respect_edit_url_overrides} || '';
    my $alternatives = $opts{alternatives} || [];
    my $alternatives_summary = $raw_dest->file('alternatives_summary.json');
    my $branch = $opts{branch};
    my $roots = $opts{roots};
    my $relativize = $opts{relativize};

    die "Can't find index [$index]" unless -f $index;

    $dest->rmtree;
    $dest->mkpath;
    $raw_dest->rmtree;
    $raw_dest->mkpath;

    my ( $output, $died );

    my $chunks_path = dir("$raw_dest/.chunked");
    $chunks_path->mkpath;
    # Emulate asciidoc_dir because we use it to find shared asciidoc files
    # but asciidoctor doesn't support it.
    my $asciidoc_dir = dir('resources/asciidoc-8.6.8/')->absolute;
    # We use the admonishment images from asciidoc so add it as a resource
    # so we can find them
    push @$resources, $asciidoc_dir;
    eval {
        $output = run(
            'asciidoctor', '-v', '--trace',
            '-r' => dir('resources/asciidoctor/lib/extensions.rb')->absolute,
            '-b' => 'html5',
            '-d' => 'book',
            '-a' => 'showcomments=1',
            '-a' => "lang=$lang",
            '-a' => "source_branch=$branch",
            # Use ` to delimit monospaced literals because our docs
            # expect that
            '-a' => 'compat-mode=legacy',
            !$private ? () : ( '-a' => "private_edit_urls" ),
            !$edit_urls ? () : ( '-a' => "edit_urls=" .
                edit_urls_for_asciidoctor($edit_urls) ),
            # Disable warning on missing attributes because we have
            # missing attributes!
            # '-a' => 'attribute-missing=warn',
            '-a' => 'asciidoc-dir=' . $asciidoc_dir,
            '-a' => 'resources=' . join(',', @$resources),
            $latest ? () : ('-a' => "migration-warnings=false"),
            $respect_edit_url_overrides ? ('-a' => "respect_edit_url_overrides=true") : (),
            @{ $alternatives } ? (
                '-a' => _format_alternatives($alternatives),
                '-a' => "alternative_language_report=$raw_dest/alternatives_report.json",
                '-a' => "alternative_language_summary=$alternatives_summary",
            ) : (),
            $relativize ? ('-a' => 'relativize-link=https://www.elastic.co/') : (),
            roots_opts( $roots ),
            # Turn off style options because we'll provide our own
            '-a' => 'stylesheet!',
            '-a' => 'icons!',
            # Turn off asciidoctor's default footer because we make our own
            '-a' => 'nofooter',
            # Pass chunking down
            '-a' => 'chunk_level=' . ( $chunk + 1 ),
            # Render the table of contents
            '-a' => 'toc',
            # Lock the destination file name to one we expect
            '--out-file' => 'index.html',
            # Asciidoctor doesn't pass the destination directory down to
            # the converter so we do so here explicitly
            '-a' => 'outdir=' . $raw_dest,
            # Add some metadata
            '-a' => 'dc.type=Learn/Docs/' . $section,
            '-a' => 'dc.subject=' . $subject,
            '-a' => 'dc.identifier=' . $version,
            $multi ? ( '-a' => "title-extra= [$version]" ) : (),
            $noindex ? ('-a' => 'noindex') : (),
            $page_header ? ('-a' => "page-header=$page_header") : (),
            '--destination-dir=' . $raw_dest,
            docinfo($index),
            $index
        );
        1;
    } or do { $output = $@; $died = 1; };
    _check_build_error( $output, $died, $lenient );

    # Extract the TOC from the index.html page *before* we (potentially) replace
    # the TOC on the index.html page with a custom title page.
    extract_toc_from_index( $raw_dest );

    _customize_title_page( $index, $raw_dest->file('index.html'), $single );
    finish_build( $index->parent, $raw_dest, $dest, $lang, 0 );
}

#===================================
sub build_single {
#===================================
    my ( $index, $raw_dest, $dest, %opts ) = @_;

    my $single = 1;
    my $type = $opts{type} || 'book';
    my $toc = $opts{toc} || '';
    my $lenient   = $opts{lenient}       || '';
    my $version   = $opts{version}       || '';
    my $multi     = $opts{multi}         || 0;
    my $lang      = $opts{lang}          || 'en';
    my $edit_urls = $opts{edit_urls};
    my $section   = $opts{section_title} || '';
    my $subject   = $opts{subject}       || '';
    my $private   = $opts{private}       || '';
    my $noindex   = $opts{noindex}       || '';
    my $resources = $opts{resource}      || [];
    my $page_header = custom_header($index) || $opts{page_header} || '';
    my $latest    = $opts{latest};
    my $respect_edit_url_overrides = $opts{respect_edit_url_overrides} || '';
    my $alternatives = $opts{alternatives} || [];
    my $alternatives_summary = $raw_dest->file('alternatives_summary.json');
    my $branch = $opts{branch};
    my $roots = $opts{roots};
    my $relativize = $opts{relativize};
    my $extra = $opts{extra} || 0;

    die "Can't find index [$index]" unless -f $index;

    unless ( $opts{is_toc} ) {
        # Usually books live in their own directory so we can just `rm -rf`
        # those directories and start over. But the Table of Contents for all
        # vrsions of a book is written to the directory that contains all of
        # the versions of that book. `rm -rf`ed there we'd lose all of the
        # versions of the book. So we just don't.
        $dest->rmtree;
        $dest->mkpath;
        $raw_dest->rmtree;
        $raw_dest->mkpath;
    }

    my ( $output, $died );

    # Emulate asciidoc_dir because we use it to find shared asciidoc files
    # but asciidoctor doesn't support it.
    my $asciidoc_dir = dir('resources/asciidoc-8.6.8/')->absolute;
    # We use the admonishment images from asciidoc so add it as a resource
    # so we can find them
    push @$resources, $asciidoc_dir;
    eval {
        $output = run(
            'asciidoctor', '-v', '--trace',
            '-r' => dir('resources/asciidoctor/lib/extensions.rb')->absolute,
            '-b' => 'html5',
            '-d' => $type,
            '-a' => 'showcomments=1',
            '-a' => "lang=$lang",
            '-a' => "source_branch=$branch",
            $private || !$edit_urls ? () : ( '-a' => "edit_urls=" .
                edit_urls_for_asciidoctor($edit_urls) ),
            '-a' => 'asciidoc-dir=' . $asciidoc_dir,
            '-a' => 'resources=' . join(',', @$resources),
            $latest ? () : ('-a' => "migration-warnings=false"),
            $respect_edit_url_overrides ? ('-a' => "respect_edit_url_overrides=true") : (),
            @{ $alternatives } ? (
                '-a' => _format_alternatives($alternatives),
                '-a' => "alternative_language_report=$raw_dest/alternatives_report.json",
                '-a' => "alternative_language_summary=$alternatives_summary",
            ) : (),
            # Disable warning on missing attributes because we have
            # missing attributes!
            # '-a' => 'attribute-missing=warn',
            $relativize ? ('-a' => 'relativize-link=https://www.elastic.co/') : (),
            roots_opts( $roots ),
            # Turn off style options because we'll provide our own
            '-a' => 'stylesheet!',
            '-a' => 'icons!',
            # Turn off asciidoctor's default footer because we make our own
            '-a' => 'nofooter',
            # Add some metadata
            '-a' => 'dc.type=Learn/Docs/' . $section,
            '-a' => 'dc.subject=' . $subject,
            '-a' => 'dc.identifier=' . $version,
            $multi ? ( '-a' => "title-extra= [$version]" ) : (),
            $noindex ? ('-a' => 'noindex') : (),
            $page_header ? ('-a' => "page-header=$page_header") : (),
            # Turn on asciidoctor's table of contents generation if we want a TOC
            $toc ? ('-a' => 'toc') : (),
            '--destination-dir=' . $raw_dest,
            docinfo($index),
            $index
        );
        1;
    } or do { $output = $@; $died = 1; };
    _check_build_error( $output, $died, $lenient );

    my $base_name = $index->basename;
    $base_name =~ s/\.[^.]+$/.html/;

    my $html_file = $raw_dest->file('index.html');
    if ( $base_name ne 'index.html' ) {
        my $src = $raw_dest->file($base_name);
        rename $src, $html_file
            or die "Couldn't rename <$src> to <index.html>: $!";
    }

    _customize_title_page( $index, $html_file, $single );

    if ( $extra ) {
        my $contents = $html_file->slurp( iomode => '<:encoding(UTF-8)' );
        $contents =~ s{<!--EXTRA-->}{<!--EXTRA-->\n<div id="extra">\n$extra\n</div>} or
            die "Couldn't add toc_extra to $contents";
        $html_file->spew( iomode => '>:utf8', $contents );
    }

    finish_build( $index->parent, $raw_dest, $dest, $lang, $opts{is_toc} );
}

#===================================
# Customize a book's title page.
#
# By default, the title page shows just the table of contents.
#
# If a file exists named `<index>-custom-title-page.html`, the table of contents
# will be replaced by the contents of this file.  Otherwise, if a file named
# `<index>-extra-title-page.html` exists, its contents will be added above the
# table of contents. (Note that the `<index>` may be different based on the
# book's "index" file.
#===================================
sub _customize_title_page {
#===================================
    my ( $index, $html_file, $single ) = @_;
    my $index_basename = $index->basename;

    (my $custom_title_page = $index_basename) =~ s/(\.x)?\.a(scii)?doc$/-custom-title-page.html/ || die;
    $custom_title_page = $index->parent->file( $custom_title_page );

    (my $extra_title_page = $index_basename) =~ s/(\.x)?\.a(scii)?doc$/-extra-title-page.html/ || die;
    $extra_title_page = $index->parent->file( $extra_title_page );

    if ( -e $custom_title_page ) {
        die "Using a custom title page is incompatible with --single" if $single;
        die "Cannot have both custom and extra title pages for the same source file" if -e $extra_title_page;

        my $custom_contents = $custom_title_page->slurp( iomode => '<:encoding(UTF-8)' );
        my $contents = $html_file->slurp( iomode => '<:encoding(UTF-8)' );
        $contents =~ s|<!--START_TOC-->.*<!--END_TOC-->|$custom_contents|sm or
            die "Couldn't set custom contents in file";
        $html_file->spew( iomode => '>:utf8', $contents );
    }
    elsif ( -e $extra_title_page ) {
        my $extra_contents = $extra_title_page->slurp( iomode => '<:encoding(UTF-8)' );
        my $contents = $html_file->slurp( iomode => '<:encoding(UTF-8)' );
        # Wrapping the extra in a div is a relic of docbook. But we're trying
        # to emulate docbook so here we are.
        $contents =~ s|</h1></div>|</h1></div>\n<div>\n$extra_contents\n</div>| or
            die "Couldn't add extra-title-page to $contents";
        $html_file->spew( iomode => '>:utf8', $contents );
    }
}

#===================================
sub _check_build_error {
#===================================
    my ( $output, $died, $lenient ) = @_;

    $output =~ s/INFO: possible invalid reference: /WARNING: invalid reference: /;
    my @lines = split "\n", $output;
    my @build_warnings = grep {/^(a2x|asciidoc(tor)?): (WARNING|ERROR):/} @lines;
    my $warned = @build_warnings;
    return unless $died || $warned;

    my @warn = grep { /(WARNING|ERROR):/ || !/^(a2x|asciidoc(tor)?): / } @lines;

    if ( $died || $warned && !$lenient ) {
        die join "\n", ( '', @warn, '' );
    }
    warn join "\n", ( '', @warn, '' );
}

#===================================
sub run (@) {
#===================================
    my @args = @_;
    my ( $out, $err, $ok );

    if ( $Opts->{verbose} ) {
        say 'Running: ' . join(' ', map { "\"$_\"" } @args);
        ( $out, $err, $ok ) = tee { system(@args) == 0 };
    }
    else {
        ( $out, $err, $ok ) = capture { system(@args) == 0 };
    }

    my $combined = "$out\n$err";
    $combined =~ s/^\s+|\s+$//g;
    return $combined if $ok;

    my $git_dir = $ENV{GIT_DIR} ? "in GIT_DIR $ENV{GIT_DIR}" : "";
    die "Error executing: @args $git_dir\n---out---\n$out\n---err---\n$err\n---------\n"
        unless $ok;

    return $combined;
}

#===================================
sub finish_build {
#===================================
    my ( $source, $raw_dest, $dest, $lang, $is_toc ) = @_;

    # Write a file with the book's language into the raw directory so the
    # templating can apply it now *and* on the fly later
    $raw_dest->file('lang')->spew( iomode => '>:utf8', "$lang\n" );

    # Apply template to HTML files
    run 'node', 'template/cli.js', '--template', 'resources/web/template.html',
        '--source', $raw_dest, '--dest', $dest,
        $is_toc ? ('--tocmode') : ();

    my $snippets_dest = $dest->subdir('snippets');
    my $snippets_src;
}

#===================================
sub extract_toc_from_index {
#===================================
    my $dir = shift;
    my $html
        = $dir->file('index.html')->slurp( 'iomode' => '<:encoding(UTF-8)' );
    $html =~ s/^.+<!--START_TOC-->\n?//s;
    $html =~ s/<!--END_TOC-->.*$//s;
    $dir->file('toc.html')->spew( iomode => '>:utf8', $html );
}

#===================================
sub roots_opts {
#===================================
    my $roots = shift;
    my @result;

    for ( keys %$roots ) {
        push @result, ( '-a', $_ . '-root=' . $$roots{ $_ } );
    }
    return @result;
}

#===================================
sub docinfo {
#===================================
    my $index = shift;
    my $name  = $index->basename;
    $name =~ s/\.[^.]+$//;
    my $docinfo = $index->dir->file("$name-docinfo.xml");
    return -e $docinfo ? ( -a => 'docinfo' ) : ();
}

#===================================
sub custom_header {
#===================================
    my $index  = shift;
    my $custom = $index->dir->file('page_header.html');
    return unless -e $custom;
    return scalar $custom->slurp( iomode => '<:encoding(UTF-8)' );
}

#===================================
sub edit_urls_for_asciidoctor {
#===================================
    my $edit_urls = shift;

    # We'd be better off using a csv library for this but we don't want to add
    # more dependencies to the pl until we go docker-only.
    return join("\n", map { "$_,$edit_urls->{$_}" } keys %{$edit_urls});
}

#===================================
sub _format_alternatives {
#===================================
    my $alternatives = shift;

    # We'd be better off using a csv library for this but it'll be ok for now.
    return 'alternative_language_lookups=' . join(
        "\n",
        map { $_->{source_lang} . ',' . $_->{alternative_lang} . ',' . $_->{dir} } @{ $alternatives }
    );
}

#===================================
sub write_html_redirect {
#===================================
    my ( $dir, $url ) = @_;
    my $html = <<"HTML";
<html>
  <head>
    <meta http-equiv="refresh" content="0; url=$url">
    <meta name="robots" content="noindex">
  </head>
  <body>
    Redirecting to <a href="$url">$url</a>.
  </body>
</html>
HTML

    $dir->file('index.html')->spew( iomode => '>:utf8', $html );
}

#===================================
# Write the redirects managed by nginx and run a basic self test on them.
#
# dest        - file to which to write the redirecs : Path::Class::file
# docs_dir    - directory containing generated docs : Path::Class::dir
# temp_dir    - directory for writing temporary files : Path::Class::file
#===================================
sub write_nginx_redirects {
#===================================
    my ( $dest, $docs_dir, $temp_dir ) = @_;

    my $redirects = dir('resources')->file('legacy_redirects.conf')
            ->slurp( iomode => "<:encoding(UTF-8)" );

    # Today we just have a list of redirects built long ago that we include
    # in the generated docs. In the future we'll generate the redirects from
    # the docs *somehow*.

    $redirects =~ s/^(#.+)?\n//gm;

    $dest->spew( iomode => '>:utf8', $redirects );

    my $test_nginx_conf = $temp_dir->file( 'nginx.conf' );
    write_nginx_test_config( $test_nginx_conf, $docs_dir, $dest, 0, 0 );
    run( qw(nginx -t -c), $test_nginx_conf );
}

#===================================
# Build an nginx config file useful for serving the docs locally or running
# a self test on the redirects.
#
# dest            - file to which to write the test config : Path::Class::file
# docs_dir        - directory containing generated docs : Path::Class::dir
# redirects_file  - file containing redirects or 0 if there aren't
#                 - any redirects : Path::Class::file||0
# waching_web     - Truthy if we are watching web resources.
# preview_enabled - Truthy if the preview application is running and we should
#                   delegate to that.
#===================================
sub write_nginx_test_config {
#===================================
    my ( $dest, $docs_dir, $redirects_file, $watching_web, $preview_enabled ) = @_;

    my $redirects_line = $redirects_file ? "include $redirects_file;\n" : '';
    my $web_conf;
    if ( $watching_web ) {
        $web_conf = <<"CONF"
    rewrite ^/guide/static/docs-v1\\.js(.*)\$ /guide/static/docs_js/index-v1.js\$1 last;
    location ^~ /guide/static/jquery.js {
      alias /node_modules/jquery/dist/jquery.js;
      types {
        application/javascript js;
      }
    }
    location ^~ /guide/static/ {
      proxy_pass http://0.0.0.0:1234;
    }
CONF
    } else {
        $web_conf = '';
    }

    my $guide_conf;
    if ( $preview_enabled ) {
        $guide_conf = <<"CONF"
    location ~/(guide|diff) {
      proxy_pass http://0.0.0.0:3000;
      proxy_http_version 1.1;
      proxy_set_header Host \$host;
      proxy_cache_bypass \$http_upgrade;
      proxy_buffering off;
      gzip on;
      add_header 'Access-Control-Allow-Origin' '*';
      if (\$request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'kbn-xsrf-token';
      }
    }
CONF
    } else {
        $guide_conf = <<"CONF"
    location /guide {
      alias $docs_dir;
      add_header 'Access-Control-Allow-Origin' '*';
      if (\$request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'kbn-xsrf-token';
      }
    }
CONF
    }

    my $nginx_conf = <<"CONF";
daemon off;
error_log /dev/stdout info;
pid /run/nginx/nginx.pid;

events {
  worker_connections 64;
}

http {
  error_log /dev/stdout crit;
  log_format short '[\$time_local] "\$request" \$status';
  access_log /dev/stdout short;

  server {
    listen 8000;
    location = / {
      return 301 /guide/index.html;
    }
$web_conf
$guide_conf
    location / {
      alias /docs_build/resources/web/static/;
      autoindex off;
    }
    types {
      application/javascript js;
      image/gif gif;
      image/jpeg jpg;
      image/jpeg jpeg;
      image/svg+xml svg;
      text/css css;
      text/html html;
    }
    rewrite ^/assets/(.+)\$ https://www.elastic.co/assets/\$1 permanent;
    rewrite ^/gdpr-data\$ https://www.elastic.co/gdpr-data permanent;
    rewrite ^/static/(.+)\$ https://www.elastic.co/static/\$1 permanent;
$redirects_line
  }
}
CONF
    $dest->spew( iomode => '>:utf8', $nginx_conf );
}

#===================================
# Build an nginx config file useful for serving a preview of all built docs.
#
# dest            - file to which to write the test config : Path::Class::file
#===================================
sub write_nginx_preview_config {
#===================================
    my ( $dest ) = @_;

    my $preview_conf = <<"CONF";
      proxy_pass http://0.0.0.0:3000;
      proxy_http_version 1.1;
      proxy_set_header Host \$host;
      proxy_cache_bypass \$http_upgrade;
      proxy_buffering off;
      gzip on;
      add_header 'Access-Control-Allow-Origin' '*';
      if (\$request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'kbn-xsrf-token';
      }
CONF
    # We log the X-Opaque-Id which is a header that Elasticsearch uses to mark
    # requests with an id that is opaque to Elasticsearch. Presumably this is
    # a standard. Either way we follow along. We use it in our tests so we can
    # figure out which request came from which test. That is the only reason
    # we *need* it right now. Presumably we'll find some other use for it later
    # though. Think of it as a distributed trace id.
    my $nginx_conf = <<"CONF";
daemon off;
error_log /dev/stdout info;
pid /run/nginx/nginx.pid;

events {
  worker_connections 64;
}

http {
  error_log /dev/stdout crit;
  log_format short '\$http_x_opaque_id \$http_host \$request \$status';
  access_log /dev/stdout short;

  server {
    listen 8000;
    location = /liveness {
      return 200 "Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn.";
    }
    location = /robots.txt {
      return 200 "User-agent: *\nDisallow: /\n";
    }
    location ~/(guide|diff) {
$preview_conf
    }
    location / {
      alias /docs_build/resources/web/static/;
      try_files \$uri \@preview;
      autoindex off;
    }
    location \@preview {
$preview_conf
    }
    rewrite ^/assets/(.+)\$ https://www.elastic.co/assets/\$1 permanent;
    rewrite ^/gdpr-data\$ https://www.elastic.co/gdpr-data permanent;
    rewrite ^/static/(.+)\$ https://www.elastic.co/static/\$1 permanent;
  }
}
CONF
    $dest->spew( iomode => '>:utf8', $nginx_conf );
}

#===================================
sub proc_man {
#===================================
    my ( $procs, $finish ) = @_;
    my $pm = Parallel::ForkManager->new($procs);
    $pm->set_waitpid_blocking_sleep(0.1);
    $pm->run_on_finish(
        sub {
            if ( $_[1] ) {
                kill -9, $pm->running_procs();
                kill 9,  $pm->running_procs();
                die "Child exited with $_[1]";
            }
            $finish->(@_) if $finish;
        }
    );
    return $pm;

}

#===================================
sub timestamp {
#===================================
    my ( $sec, $min, $hour, $mday, $mon, $year )
        = gmtime( @_ ? shift() : time() );
    $year += 1900;
    $mon++;
    sprintf "%04d-%02d-%02dT%02d:%02d:%02d+00:00", $year, $mon, $mday, $hour,
        $min, $sec;
}

#===================================
sub start_web_resources_watcher {
#===================================
    my $parcel_pid = fork;
    return $parcel_pid if $parcel_pid;

    close STDIN;
    open( STDIN, "</dev/null" );
    exec( qw(/node_modules/parcel/bin/cli.js serve
             --public-url /guide/static/
             --hmr-port 8001
             -d /tmp/parcel/
             resources/web/docs_js/index-v1.js resources/web/styles-v1.pcss) );
}

#===================================
sub start_preview {
#===================================
    my ( $command, $root, $default_template, $ignore_host ) = @_;

    my $preview_pid = fork;
    return $preview_pid if $preview_pid;

    close STDIN;
    open( STDIN, "</dev/null" );
    exec( qw(node --max-old-space-size=128 /docs_build/preview/cli.js),
          $command, $root,
          '--default-template', $default_template,
          ( $ignore_host ? ('--ignore-host') : () )
    );
}

#===================================
sub build_web_resources {
#===================================
    my ( $dest ) = @_;

    my $parcel_out = dir('/tmp/parcel');
    my $compiled_js = $parcel_out->file('docs_js/index-v1.js');
    my $compiled_css = $parcel_out->file('styles-v1.css');

    unless ( -e $compiled_js && -e $compiled_css ) {
        # We write the compiled js and css to /tmp so we can use them on
        # subsequent runs in the same container. This doesn't come up when you
        # build docs either with --doc or --all *but* it comes up all the time
        # when you run the integration tests and saves about 1.5 seconds on
        # every docs build.
        say "Compiling web resources";
        run '/node_modules/parcel/bin/cli.js', 'build',
            '--public-url', '/guide/static/',
            '--experimental-scope-hoisting', '--no-source-maps',
            '-d', $parcel_out,
            'resources/web/docs_js/index-v1.js', 'resources/web/styles-v1.pcss';
        die "Parcel didn't make $compiled_js" unless -e $compiled_js;
        die "Parcel didn't make $compiled_css" unless -e $compiled_css;
    }

    my $static_dir = $dest->subdir( 'raw' )->subdir( 'static' );
    $static_dir->mkpath;
    my $js = $static_dir->file( 'docs-v1.js' );
    my $css = $static_dir->file( 'styles-v1.css' );
    my $js_licenses = file( 'resources/web/docs.js.licenses' );
    my $css_licenses = file( 'resources/web/styles.css.licenses' );
    $js->spew(
        iomode => '>:utf8',
        $js_licenses->slurp( iomode => '<:encoding(UTF-8)' ) . $compiled_js->slurp( iomode => '<:encoding(UTF-8)' )
    );
    $css->spew(
        iomode => '>:utf8',
        $css_licenses->slurp( iomode => '<:encoding(UTF-8)' ) . $compiled_css->slurp( iomode => '<:encoding(UTF-8)' )
    );

    for ( $parcel_out->children ) {
        next unless /.+\.woff2?/;
        rcopy( $_, $static_dir );
    }

    rcopy( '/node_modules/jquery/dist/jquery.min.js', $static_dir->file( 'jquery.js' ) );

    # The public site can't ready anything from the raw directory so we have to
    # copy the static files to html as well.
    my $templated_dir = $dest->subdir( 'html' )->subdir( 'static' );
    $templated_dir->mkpath;
    rcopy( $static_dir, $templated_dir );

    # Copy the template to the root of the repo so we can apply it on the fly.
    # NOTE: We only apply it on the fly for preview right now.
    for ( qw(template air_gapped_template) ) {
        my $template_source = file("resources/web/$_.html");
        my $template = $dest->file("$_.html");
        rcopy( $template_source, $template );
    }
}

1
