diff --git a/doc/doc.pl b/doc/doc.pl
index ec1d97598..b318e2fa2 100755
--- a/doc/doc.pl
+++ b/doc/doc.pl
@@ -355,6 +355,9 @@ eval
}
}
+ # Verify page/section links now that all pages have been rendered and their section anchors are known
+ $oManifest->linkVerify();
+
# Cache the manifest (mostly useful for testing rendering changes in the code)
if (!$bNoCache && !$bCacheOnly)
{
diff --git a/doc/lib/pgBackRestDoc/Common/DocManifest.pm b/doc/lib/pgBackRestDoc/Common/DocManifest.pm
index 8dd371b4f..6f3a69dd4 100644
--- a/doc/lib/pgBackRestDoc/Common/DocManifest.pm
+++ b/doc/lib/pgBackRestDoc/Common/DocManifest.pm
@@ -620,6 +620,38 @@ sub renderOutGet
);
}
+####################################################################################################################################
+# linkVerify
+#
+# Verify that every page/section link points at a section that exists. This must be done after all pages have been rendered since
+# the section anchors for a page are only known once it has been rendered. Links to a page that was not rendered in this build
+# (e.g. a partial build with --include/--exclude) are skipped since the section anchors are not available to check.
+####################################################################################################################################
+sub linkVerify
+{
+ my $self = shift;
+
+ # Assign function parameters, defaults, and log debug info
+ my ($strOperation) = logDebugParam(__PACKAGE__ . '->linkVerify');
+
+ foreach my $rhLink (@{$self->{rhyLinkVerify} || []})
+ {
+ # Skip if the linked page was not rendered in this build
+ next if (!defined($self->{hPageAnchor}{$rhLink->{page}}));
+
+ # Error if the section does not exist on the page
+ if (!defined($self->{hPageAnchor}{$rhLink->{page}}{substr($rhLink->{section}, 1)}))
+ {
+ confess &log(
+ ERROR,
+ "page '$rhLink->{source}' has a link to page '$rhLink->{page}' section '$rhLink->{section}' that does not exist");
+ }
+ }
+
+ # Return from function and log return values if any
+ return logDebugReturn($strOperation);
+}
+
####################################################################################################################################
# cacheKey
####################################################################################################################################
diff --git a/doc/lib/pgBackRestDoc/Common/DocRender.pm b/doc/lib/pgBackRestDoc/Common/DocRender.pm
index 939b3dc9e..833cf41fb 100644
--- a/doc/lib/pgBackRestDoc/Common/DocRender.pm
+++ b/doc/lib/pgBackRestDoc/Common/DocRender.pm
@@ -200,6 +200,9 @@ sub new
$self->required($strPath);
}
}
+
+ # Register the section anchor map so cross-page links to this page can be verified
+ $self->{oManifest}->{hPageAnchor}{$self->{strRenderOutKey}} = $self->sectionAnchorMap();
}
if (defined($self->{oDoc}))
@@ -588,6 +591,40 @@ sub isRequired
defined(${$self->{oSectionRequired}}{$strPath}) ? true : false;
}
+####################################################################################################################################
+# sectionAnchorMap
+#
+# Build the set of section anchors for this page. An anchor is the section id prefixed with the parent anchors, which matches the
+# anchor generated by the html renderer. A section with anchor set to no-inherit does not include its parent anchors.
+####################################################################################################################################
+sub sectionAnchorMap
+{
+ my $self = shift;
+
+ my $rhAnchor = {};
+
+ foreach my $strPath (keys(%{$self->{oSection}}))
+ {
+ my @stryId;
+ my $oSection = ${$self->{oSection}}{$strPath};
+
+ # Prepend section ids walking up the parents until a section that does not inherit the parent anchor
+ while (defined($oSection))
+ {
+ unshift(@stryId, $oSection->paramGet('id'));
+
+ last if $oSection->paramTest(XML_SECTION_PARAM_ANCHOR, XML_SECTION_PARAM_ANCHOR_VALUE_NOINHERIT);
+
+ my $strParentPath = $oSection->paramGet('path-parent', false);
+ $oSection = defined($strParentPath) ? ${$self->{oSection}}{$strParentPath} : undef;
+ }
+
+ $rhAnchor->{join('/', @stryId)} = true;
+ }
+
+ return $rhAnchor;
+}
+
####################################################################################################################################
# processTag
####################################################################################################################################
@@ -627,6 +664,12 @@ sub processTag
my $strPage = $self->variableReplace($oTag->paramGet('page', false));
my $strSection = $oTag->paramGet('section', false);
+ # A section link must begin with / since the leading / is stripped to form the anchor
+ if (defined($strSection) && substr($strSection, 0, 1) ne '/')
+ {
+ confess &log(ERROR, "link section '${strSection}' must begin with '/'");
+ }
+
# If a page/section link points to the current page then remove the page portion
if (defined($strPage) && defined($strSection) && defined($self->{strRenderOutKey}) &&
$strPage eq $self->{strRenderOutKey})
@@ -637,6 +680,15 @@ sub processTag
# If this is a page URL
if (defined($strPage))
{
+ # Record the page/section link so it can be verified once all pages have been rendered and their section
+ # anchors are known
+ if (defined($strSection))
+ {
+ push(
+ @{$self->{oManifest}->{rhyLinkVerify}},
+ {source => $self->{strRenderOutKey}, page => $strPage, section => $strSection});
+ }
+
# If the page wasn't rendered then point at the website
if (!defined($self->{oManifest}->renderOutGet($strType, $strPage, true)))
{
diff --git a/doc/xml/news.xml b/doc/xml/news.xml
index 4047d144c..21397ab46 100644
--- a/doc/xml/news.xml
+++ b/doc/xml/news.xml
@@ -49,7 +49,7 @@
See the 2.59.0 Release Notes for additional features and improvements.
+See the 2.59.0 Release Notes for additional features and improvements.