From c8f4192c4adb56f16be67071a13dd4fcd674cc23 Mon Sep 17 00:00:00 2001 From: David Steele Date: Tue, 21 Jul 2026 21:31:15 +0700 Subject: [PATCH] Verify that page/section links point at an existing section. Only same-page section links were verified, so a page/section link with a bad section or a missing leading slash would silently render a broken anchor. Require a link section to begin with a slash and, after all pages have been rendered, verify that each page/section link points at a section that exists on the target page. Links to a page that is not part of the build (e.g. a partial build) are skipped. Also fix the news link to the 2.59.0 release notes, which was missing the leading slash and pointed at a broken anchor. --- doc/doc.pl | 3 ++ doc/lib/pgBackRestDoc/Common/DocManifest.pm | 32 +++++++++++++ doc/lib/pgBackRestDoc/Common/DocRender.pm | 52 +++++++++++++++++++++ doc/xml/news.xml | 2 +- 4 files changed, 88 insertions(+), 1 deletion(-) 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 @@ Exit async archive-push on first error (David Steele) -

See the 2.59.0 Release Notes for additional features and improvements.

+

See the 2.59.0 Release Notes for additional features and improvements.