lib/ES/Book.pm (408 lines of code) (raw):
package ES::Book;
use strict;
use warnings;
use v5.10;
use Data::Dumper qw(Dumper);
use ES::Util
qw(run build_chunked build_single proc_man write_html_redirect $Opts);
use Path::Class();
use ES::Source();
use File::Copy::Recursive qw(fcopy rcopy);
use ES::Toc();
use utf8;
use List::Util qw(first);
our %Page_Header = (
en => {
old => <<"HEADER",
A newer version is available. For the latest information, see the
<a href="../current/index.html">current release documentation</a>.
HEADER
dead => <<"HEADER",
<strong>IMPORTANT</strong>: No additional bug fixes or documentation updates
will be released for this version. For the latest information, see the
<a href="../current/index.html">current release documentation</a>.
HEADER
new => <<"HEADER"
This documentation contains work-in-progress information for future Elastic Stack and Cloud releases. Use the version selector to view supported release docs. It also contains some Elastic Cloud serverless information. Check out our <a href="https://www.elastic.co/docs/current/serverless">serverless docs</a> for more details.
HEADER
},
zh_cn => {
old => <<"HEADER",
你当前正在查看的是旧版本的文档。如果不是你要找的,请点击查看 <a href="../current/index.html">当前发布版本的文档</a>。
HEADER
dead => <<"HEADER",
你当前正在查看的是旧版本的文档。如果不是你要找的,请点击查看 <a href="../current/index.html">当前发布版本的文档</a>。
HEADER
new => <<"HEADER"
你当前正在查看的是未发布版本的预览版文档。如果不是你要找的,请点击查看 <a href="../current/index.html">当前发布版本的文档</a>。
HEADER
},
ja => {
old => <<"HEADER",
A newer version is available. For the latest information, see the
<a href="../current/index.html">current release documentation</a>.
HEADER
dead => <<"HEADER",
<strong>IMPORTANT</strong>: No additional bug fixes or documentation updates
will be released for this version. For the latest information, see the
<a href="../current/index.html">current release documentation</a>.
HEADER
new => <<"HEADER"
This documentation contains work-in-progress information for future Elastic Stack and Cloud releases. Use the version selector to view supported release docs. It also contains some Elastic Cloud serverless information. Check out our <a href="https://www.elastic.co/docs/current/serverless">serverless docs</a> for more details.
HEADER
},
ko => {
old => <<"HEADER",
A newer version is available. For the latest information, see the
<a href="../current/index.html">current release documentation</a>.
HEADER
dead => <<"HEADER",
<strong>IMPORTANT</strong>: No additional bug fixes or documentation updates
will be released for this version. For the latest information, see the
<a href="../current/index.html">current release documentation</a>.
HEADER
new => <<"HEADER"
This documentation contains work-in-progress information for future Elastic Stack and Cloud releases. Use the version selector to view supported release docs. It also contains some Elastic Cloud serverless information. Check out our <a href="https://www.elastic.co/docs/current/serverless">serverless docs</a> for more details.
HEADER
}
);
#===================================
sub new {
#===================================
my ( $class, %args ) = @_;
my $title = $args{title}
or die "No <title> specified: " . Dumper( \%args );
my $source = ES::Source->new(
temp_dir => $args{temp_dir},
sources => $args{sources},
examples => $args{examples},
);
my $prefix = $args{prefix}
or die "No <prefix> specified for book <$title>";
my $index = Path::Class::file( $args{index} || 'index.asciidoc' );
my $chunk = $args{chunk} || 0;
my $toc = $args{toc} || 0;
my $branch_list = $args{branches};
my $current = $args{current};
die "<branches> must be an array in book <$title>"
unless ref $branch_list eq 'ARRAY';
# Each branch can be either a single value, or a mapping of
# {<branch_name>: <title>}. Branch titles are used in the version dropdown
# and version lists.
my ( @branches, %branch_titles );
for (@$branch_list) {
my ( $branch, $title ) = ref $_ eq 'HASH' ? (%$_) : ( $_, $_ );
push @branches, $branch;
$branch_titles{$branch} = $title;
}
die "Current branch <$current> is not in <branches> in book <$title>"
unless $branch_titles{$current};
my $live_branches = $args{live};
# If `live` is defined, check if there are any specified branches that
# aren't in the list of branches being built.
my @difference;
foreach my $item (@$live_branches) {
push @difference, $item unless grep { $item eq $_ } @branches;
}
# print "Branches: ", join(", ", @branches), "\n";
# print "Live: ", join(", ", @$live_branches), "\n";
# print "Difference: ", join(", ", @difference), "\n";
my $missing = join ", ", @difference;
die "Live branch(es) <$missing> not in <branches> in book <$title>"
if $difference[0];
my $tags = $args{tags}
or die "No <tags> specified for book <$title>";
my $subject = $args{subject}
or die "No <subject> specified for book <$title>";
my $lang = $args{lang} || 'en';
my $respect_edit_url_overrides = 0;
if (exists $args{respect_edit_url_overrides}) {
$respect_edit_url_overrides = $args{respect_edit_url_overrides};
if ($respect_edit_url_overrides eq 'true') {
$respect_edit_url_overrides = 1;
} elsif ($respect_edit_url_overrides eq 'false') {
$respect_edit_url_overrides = 0;
} else {
die 'respect_edit_url_overrides must be true or false but was ' . $respect_edit_url_overrides;
}
}
bless {
title => $title,
raw_dir => $args{raw_dir}->subdir( $prefix ),
dir => $args{dir}->subdir( $prefix ),
temp_dir => $args{temp_dir},
source => $source,
prefix => $prefix,
chunk => $chunk,
toc => $toc,
single => $args{single},
index => $index,
branches => \@branches,
live_branches => $args{live} || \@branches,
branch_titles => \%branch_titles,
current => $current,
tags => $tags,
subject => $subject,
private => $args{private} || '',
noindex => $args{noindex} || '',
lang => $lang,
respect_edit_url_overrides => $respect_edit_url_overrides,
suppress_migration_warnings => $args{suppress_migration_warnings} || 0,
toc_extra => $args{toc_extra} || '',
}, $class;
}
#===================================
sub build {
#===================================
my ( $self, $rebuild, $conf_path ) = @_;
my $toc_extra = $self->{toc_extra} ? $conf_path->parent->file( $self->{toc_extra} ) : 0;
my $toc = ES::Toc->new( $self->title, $toc_extra );
my $dir = $self->dir;
$dir->mkpath;
my $title = $self->title;
my $pm = proc_man(
$Opts->{procs},
sub {
my ( $pid, $error, $branch ) = @_;
$self->source->mark_done( $title, $branch );
}
);
my $latest = !$self->{suppress_migration_warnings};
my $update_version_toc = 0;
my $rebuilding_current_branch = 0;
for my $branch ( @{ $self->branches } ) {
my $building = $self->_build_book( $branch, $pm, $rebuild, $latest );
$update_version_toc ||= $building;
$latest = 0;
my $version = $self->branch_title($branch);
$toc->add_entry(
{ title => "$title: $version",
url => "$version/index.html"
}
);
if ( $branch eq $self->current ) {
$rebuilding_current_branch = $building;
}
}
$pm->wait_all_children();
$self->_copy_branch_to_current if $rebuilding_current_branch;
$update_version_toc |= $self->_remove_old_versions;
if ( $self->is_multi_version ) {
if ( $update_version_toc ) {
# We could get away with only doing this if we added or removed
# any branches or changed the current branch, but we don't have
# that information right now.
$toc->write( $self->{raw_dir}, $dir, $self->{temp_dir} );
for ( @{ $self->branches } ) {
my $version = $self->branch_title($_);
$self->_update_title_and_version_drop_downs( $dir->subdir( $version ), $_ );
}
$self->_update_title_and_version_drop_downs( $dir->subdir( 'current' ) , $self->current );
for ( @{ $self->branches } ) {
my $version = $self->branch_title($_);
$self->_update_title_and_version_drop_downs( $self->{raw_dir}->subdir( $version ), $_ );
}
$self->_update_title_and_version_drop_downs( $self->{raw_dir}->subdir( 'current' ) , $self->current );
}
return {
title => "$title [" . $self->branch_title( $self->current ) . "\\]",
url => $self->prefix . '/current/index.html',
versions => $self->prefix . '/index.html',
section_title => $self->section_title()
};
}
if ( $update_version_toc ) {
write_html_redirect( $dir, "current/index.html" );
}
return {
title => $title,
url => $self->prefix . '/current/index.html'
};
}
#===================================
# Fork a process to build the book if it needs to be built. Returns 0
# immediately if the book doesn't have to be built. Forks and then returns 1
# immediately if the book *does* have to be built. To get the success or
# failure of the build you must wait on the $pm argument for the children to
# join the parent process.
#
# branch - The branch being built ## TODO: Change to `version`
# pm - ProcessManager for forking
# rebuild - if truthy then we rebuild the book regardless of changes.
# latest - is this the latest branch of the book?
#===================================
sub _build_book {
#===================================
my ( $self, $branch, $pm, $rebuild, $latest ) = @_;
my $version = $self->branch_title($branch);
my $raw_version_dir = $self->{raw_dir}->subdir( $version );
my $version_dir = $self->dir->subdir($version);
my $source = $self->source;
my $index = $self->index;
my $section_title = $self->section_title($version);
my $subject = $self->subject;
my $lang = $self->lang;
return 0 unless $rebuild ||
$source->has_changed( $self->title, $branch );
my ( $checkout, $edit_urls, $first_path, $alternatives, $roots ) =
$source->prepare($self->title, $branch);
$pm->start($branch) and return 1;
printf(" - %40.40s: Building %s...\n", $self->title, $version);
eval {
if ( $self->single ) {
build_single(
$first_path->file($index),
$raw_version_dir,
$version_dir,
version => $version,
lang => $lang,
edit_urls => $edit_urls,
private => $self->private( $branch ),
noindex => $self->noindex( $branch ),
multi => $self->is_multi_version,
page_header => $self->_page_header($branch),
section_title => $section_title,
subject => $subject,
toc => $self->toc,
resource => [$checkout],
latest => $latest,
respect_edit_url_overrides => $self->{respect_edit_url_overrides},
alternatives => $alternatives,
branch => $branch,
roots => $roots,
relativize => 1,
);
}
else {
build_chunked(
$first_path->file($index),
$raw_version_dir,
$version_dir,
version => $version,
lang => $lang,
edit_urls => $edit_urls,
private => $self->private( $branch ),
noindex => $self->noindex( $branch ),
chunk => $self->chunk,
multi => $self->is_multi_version,
page_header => $self->_page_header($branch),
section_title => $section_title,
subject => $subject,
resource => [$checkout],
latest => $latest,
respect_edit_url_overrides => $self->{respect_edit_url_overrides},
alternatives => $alternatives,
branch => $branch,
roots => $roots,
relativize => 1,
);
}
$checkout->rmtree;
printf(" - %40.40s: Finished %s\n", $self->title, $version);
1;
} && $pm->finish;
# NOTE: This method is about a screen up with $pm->start so it doesn't
# return *anything* here. It just dies if there was a failure so we can
# pick that up in the parent process.
my $error = $@;
die "\nERROR building "
. $self->title
. " version $version\n\n"
. $source->dump_recent_commits( $self->title, $branch )
. $error . "\n";
}
#===================================
sub _update_title_and_version_drop_downs {
#===================================
my ( $self, $version_dir, $branch ) = @_;
my $title = '<li id="book_title"><span>' . $self->title . ': ';
$title .= '<select id="live_versions">';
my $removed_any = 0;
for my $b ( @{ $self->branches } ) {
my $live = grep( /^$b$/, @{ $self->{live_branches} } );
unless ( $live || $branch eq $b ) {
$removed_any = 1;
next;
}
my $version = $self->branch_title($b);
$title .= '<option value="' . $version . '"';
$title .= ' selected' if $branch eq $b;
$title .= '>' . $version;
$title .= '</option>';
}
$title .= '<option value="other">other versions</option>' if $removed_any;
$title .= '</select>';
if ( $removed_any ) {
$title .= '<span id="other_versions">other versions: <select>';
for my $b ( @{ $self->branches } ) {
my $version = $self->branch_title($b);
$title .= '<option value="' . $version . '"';
$title .= ' selected' if $branch eq $b;
$title .= '>' . $version;
$title .= '</option>';
}
$title .= '</select>';
}
$title .= '</span></li>';
for ( 'toc.html', 'index.html' ) {
my $file = $version_dir->file($_);
# Ignore missing files because the books haven't been built yet. This
# can happen after a new branch is added to the config and then we use
# --keep_hash to prevent building new books, like for PR tests.
next unless -e $file;
my $html = $file->slurp( iomode => "<:encoding(UTF-8)" );
# If a book uses a custom index page, it may not include the TOC. The
# substitution below will fail, so we abort early in this case.
next unless ($_ == 'index.html' && ($html =~ /ul class="toc"/));
my $success = ($html =~ s/<ul class="toc">(?:<li id="book_title">.+?<\/li>)?\n?<li>/<ul class="toc">${title}<li>/);
die "couldn't update version" unless $success;
$file->spew( iomode => '>:utf8', $html );
}
}
#===================================
sub _copy_branch_to_current {
#===================================
my ( $self ) = @_;
# TODO: current should be a version, not a branch
my $version_dir = $self->{dir}->subdir( $self->branch_title( $self->current ) );
my $current_dir = $self->{dir}->subdir('current');
my $raw_version_dir = $self->{raw_dir}->subdir( $self->branch_title( $self->current ) );
my $raw_current_dir = $self->{raw_dir}->subdir('current');
$current_dir->rmtree;
rcopy( $version_dir, $current_dir )
or die "Couldn't copy <$version_dir> to <$current_dir>: $!";
$raw_current_dir->rmtree;
rcopy( $raw_version_dir, $raw_current_dir )
or die "Couldn't copy <$raw_version_dir> to <$raw_current_dir>: $!";
}
#===================================
sub _page_header {
#===================================
my ( $self, $branch ) = @_;
return '' unless $self->is_multi_version;
my $current = $self->current;
return '' if $current eq $branch;
# Find the positions of the branch being built ($branch) and the current
# branch ($current) in the list of branches for this book.
my @branches = @{$self->branches};
my $branchidx = first { $branches[$_] eq $branch } 0..$#branches;
my $currentidx = first { $branches[$_] eq $current } 0..$#branches;
# Old branches are "later" in the list than the current branch;
my $key = $branchidx > $currentidx ? 'old' : 'new';
$key = 'dead' if $key eq 'old' && !grep( /^$branch$/, @{ $self->{live_branches} } );
return $self->_page_header_text( $key );
}
#===================================
sub _page_header_text {
#===================================
my ( $self, $phrase ) = @_;
$phrase ||= '';
return $Page_Header{ $self->lang }{$phrase}
|| die "No page header available for lang: "
. $self->lang
. " and phrase: $phrase";
}
#===================================
# Remove all files for versions that have been removed.
#
# Versions are the `branch_title`s of each branch. We also want to keep the `current` version.
#===================================
sub _remove_old_versions {
#===================================
my $self = shift;
my $dir = $self->dir;
my %versions = map { $self->branch_title($_) => 1 } ( @{ $self->branches } );
$versions{'current'} = 1;
my $removed_any = 0;
for my $child ( $dir->children ) {
next unless $child->is_dir;
my $version = $child->basename;
# Don't delete any version that is "current" or in the list of branches.
next if $versions{$version};
printf(" - %40.40s: Deleting old branch %s\n", $self->title, $version);
$child->rmtree;
$removed_any = 1;
}
return $removed_any;
}
#===================================
sub section_title {
#===================================
my $self = shift;
my $version = shift || '';
my $title = $self->tags;
return $title unless $self->is_multi_version;
return $title . "/" . $version;
}
#===================================
sub noindex {
#===================================
my ( $self, $branch ) = @_;
return 1 if $self->{noindex};
return 0 if grep( /^$branch$/, @{ $self->{live_branches} } );
return 1;
}
#===================================
sub private {
#===================================
my ( $self, $branch ) = @_;
return 1 if $self->{private};
return 0 if $branch =~ /^(master|main)$/;
return 0 if grep( /^$branch$/, @{ $self->{live_branches} } );
return 1;
}
#===================================
sub title { shift->{title} }
sub dir { shift->{dir} }
sub prefix { shift->{prefix} }
sub chunk { shift->{chunk} }
sub toc { shift->{toc} }
sub single { shift->{single} }
sub index { shift->{index} }
sub branches { shift->{branches} }
sub branch_title { shift->{branch_titles}->{ shift() } }
sub current { shift->{current} }
sub is_multi_version { @{ shift->branches } > 1 }
sub tags { shift->{tags} }
sub subject { shift->{subject} }
sub source { shift->{source} }
sub lang { shift->{lang} }
#===================================
1;