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;