Skip to content

Commit

Permalink
Generators/HTML: slugify anchor links
Browse files Browse the repository at this point in the history
... to prevent issues with URL encoding.

Note: as this _may_ result in duplicate anchor links, this commit includes a protection against this by adding a numeric suffix to the anchor if a duplicate is detected.

Includes a test with a variety of non-ascii chars and duplicate titles.
Includes updated test expectations for various other tests.
  • Loading branch information
jrfnl committed Mar 8, 2025
1 parent 4bc3d2e commit b2fba89
Show file tree
Hide file tree
Showing 37 changed files with 270 additions and 42 deletions.
28 changes: 27 additions & 1 deletion src/Generators/HTML.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ class HTML extends Generator
}
</style>';

/**
* List of seen slugified anchors to ensure uniqueness.
*
* @var array<string, true>
*/
private $seenAnchors = [];


/**
* Generates the documentation for a standard.
Expand All @@ -132,6 +139,10 @@ public function generate()
$content = ob_get_contents();
ob_end_clean();

// Clear anchor cache after Documentation generation.
// The anchor generation for the TOC anchor links will use the same logic, so should end up with the same unique slugs.
$this->seenAnchors = [];

if (trim($content) !== '') {
echo $this->getFormattedHeader();
echo $this->getFormattedToc();
Expand Down Expand Up @@ -325,7 +336,22 @@ public function processSniff(DOMNode $doc)
*/
private function titleToAnchor($title)
{
return str_replace(' ', '-', $title);
// Slugify the text.
$title = strtolower($title);
$title = str_replace(' ', '-', $title);
$title = preg_replace('`[^a-z0-9._-]`', '-', $title);

if (isset($this->seenAnchors[$title]) === false) {
// Add to "seen" list.
$this->seenAnchors[$title] = true;
} else {
// Try to find a unique anchor for this title.
for ($i = 2; (isset($this->seenAnchors[$title.'-'.$i]) === true); $i++);
$title .= '-'.$i;
$this->seenAnchors[$title] = true;
}

return $title;

}//end titleToAnchor()

Expand Down
10 changes: 10 additions & 0 deletions tests/Core/Generators/AnchorLinksTest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="GeneratorTest" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/PHPCSStandards/PHP_CodeSniffer/master/phpcs.xsd">

<config name="installed_paths" value="./tests/Core/Generators/Fixtures/"/>

<rule ref="StandardWithDocs.Content.DocumentationTitleToAnchorSlug1"/>
<rule ref="StandardWithDocs.Content.DocumentationTitleToAnchorSlug2"/>
<rule ref="StandardWithDocs.Content.DocumentationTitleToAnchorSlug3"/>

</ruleset>
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-blank-lines">Code Comparison, blank lines<a class="sniffanchor" href="#Code-Comparison,-blank-lines"> &sect; </a></h2>
<h2 id="code-comparison--blank-lines">Code Comparison, blank lines<a class="sniffanchor" href="#code-comparison--blank-lines"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-block-length">Code Comparison, block length<a class="sniffanchor" href="#Code-Comparison,-block-length"> &sect; </a></h2>
<h2 id="code-comparison--block-length">Code Comparison, block length<a class="sniffanchor" href="#code-comparison--block-length"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-char-encoding">Code Comparison, char encoding<a class="sniffanchor" href="#Code-Comparison,-char-encoding"> &sect; </a></h2>
<h2 id="code-comparison--char-encoding">Code Comparison, char encoding<a class="sniffanchor" href="#code-comparison--char-encoding"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-line-length">Code Comparison, line length<a class="sniffanchor" href="#Code-Comparison,-line-length"> &sect; </a></h2>
<h2 id="code-comparison--line-length">Code Comparison, line length<a class="sniffanchor" href="#code-comparison--line-length"> &sect; </a></h2>
<p class="text">Ensure there is no PHP &quot;Warning: str_repeat(): Second argument has to be greater than or equal to 0&quot;.<br/>
Ref: squizlabs/PHP_CodeSniffer#2522</p>
<table class="code-comparison">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Title,-line-wrapping">Code Title, line wrapping<a class="sniffanchor" href="#Code-Title,-line-wrapping"> &sect; </a></h2>
<h2 id="code-title--line-wrapping">Code Title, line wrapping<a class="sniffanchor" href="#code-title--line-wrapping"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Title,-whitespace-handling">Code Title, whitespace handling<a class="sniffanchor" href="#Code-Title,-whitespace-handling"> &sect; </a></h2>
<h2 id="code-title--whitespace-handling">Code Title, whitespace handling<a class="sniffanchor" href="#code-title--whitespace-handling"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="This-is-a-very-very-very-very-very-very-very-very-very-very-very-long-title">This is a very very very very very very very very very very very long title<a class="sniffanchor" href="#This-is-a-very-very-very-very-very-very-very-very-very-very-very-long-title"> &sect; </a></h2>
<h2 id="this-is-a-very-very-very-very-very-very-very-very-very-very-very-long-title">This is a very very very very very very very very very very very long title<a class="sniffanchor" href="#this-is-a-very-very-very-very-very-very-very-very-very-very-very-long-title"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<div class="tag-line">Documentation generated on #REDACTED# by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer #VERSION#</a></div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Documentation-Title-PCRE-Fallback">Documentation Title PCRE Fallback<a class="sniffanchor" href="#Documentation-Title-PCRE-Fallback"> &sect; </a></h2>
<h2 id="documentation-title-pcre-fallback">Documentation Title PCRE Fallback<a class="sniffanchor" href="#documentation-title-pcre-fallback"> &sect; </a></h2>
<p class="text">Testing the document title can get determined from the sniff name if missing.</p>
<p class="text">This file name contains an acronym on purpose to test the word splitting.</p>
<div class="tag-line">Documentation generated on #REDACTED# by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer #VERSION#</a></div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<html>
<head>
<title>GeneratorTest Coding Standards</title>
<style>
body {
background-color: #FFFFFF;
font-size: 14px;
font-family: Arial, Helvetica, sans-serif;
color: #000000;
}

h1 {
color: #666666;
font-size: 20px;
font-weight: bold;
margin-top: 0px;
background-color: #E6E7E8;
padding: 20px;
border: 1px solid #BBBBBB;
}

h2 {
color: #00A5E3;
font-size: 16px;
font-weight: normal;
margin-top: 50px;
}

h2 a.sniffanchor,
h2 a.sniffanchor {
color: #006C95;
opacity: 0;
padding: 0 3px;
text-decoration: none;
font-weight: bold;
}
h2:hover a.sniffanchor,
h2:focus a.sniffanchor {
opacity: 1;
}

.code-comparison {
width: 100%;
}

.code-comparison td {
border: 1px solid #CCCCCC;
}

.code-comparison-title, .code-comparison-code {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
color: #000000;
vertical-align: top;
padding: 4px;
width: 50%;
background-color: #F1F1F1;
line-height: 15px;
}

.code-comparison-title {
text-align: left;
font-weight: 600;
}

.code-comparison-code {
font-family: Courier;
background-color: #F9F9F9;
}

.code-comparison-highlight {
background-color: #DDF1F7;
border: 1px solid #00A5E3;
line-height: 15px;
}

.tag-line {
text-align: center;
width: 100%;
margin-top: 30px;
font-size: 12px;
}

.tag-line a {
color: #000000;
}
</style>
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2>Table of Contents</h2>
<ul class="toc">
<li><a href="#url-enc-de-non---sc-----chars">URL enc@de non-àscíï chars</a></li>
<li><a href="#url-enc-de-non---sc-----chars-2">URL enc@de non-àscíï chars</a></li>
<li><a href="#url-enc-de-non---sc-----chars-3">URL enc@de non-àscíï chars</a></li>
</ul>
<h2 id="url-enc-de-non---sc-----chars">URL enc@de non-àscíï chars<a class="sniffanchor" href="#url-enc-de-non---sc-----chars"> &sect; </a></h2>
<p class="text">The documentation title has non-ascii characters, which will be slugified for use in an HTML anchor link.</p>
<h2 id="url-enc-de-non---sc-----chars-2">URL enc@de non-àscíï chars<a class="sniffanchor" href="#url-enc-de-non---sc-----chars-2"> &sect; </a></h2>
<p class="text">The documentation title has non-ascii characters, which will be slugified for use in an HTML anchor link.<br/>
A duplicate anchor link will get a numeric suffix.</p>
<h2 id="url-enc-de-non---sc-----chars-3">URL enc@de non-àscíï chars<a class="sniffanchor" href="#url-enc-de-non---sc-----chars-3"> &sect; </a></h2>
<p class="text">The documentation title has non-ascii characters, which will be slugified for use in an HTML anchor link.<br/>
A duplicate anchor link will get a numeric suffix.</p>
<div class="tag-line">Documentation generated on #REDACTED# by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer #VERSION#</a></div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-mismatched-code-blocks">Code Comparison, mismatched code blocks<a class="sniffanchor" href="#Code-Comparison,-mismatched-code-blocks"> &sect; </a></h2>
<h2 id="code-comparison--mismatched-code-blocks">Code Comparison, mismatched code blocks<a class="sniffanchor" href="#code-comparison--mismatched-code-blocks"> &sect; </a></h2>
<p class="text">This doc has two code elements, one only has a title, one has actual code. Unbalanced</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-missing-code-element">Code Comparison, missing code element<a class="sniffanchor" href="#Code-Comparison,-missing-code-element"> &sect; </a></h2>
<h2 id="code-comparison--missing-code-element">Code Comparison, missing code element<a class="sniffanchor" href="#code-comparison--missing-code-element"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<div class="tag-line">Documentation generated on #REDACTED# by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer #VERSION#</a></div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-no-code">Code Comparison, no code<a class="sniffanchor" href="#Code-Comparison,-no-code"> &sect; </a></h2>
<h2 id="code-comparison--no-code">Code Comparison, no code<a class="sniffanchor" href="#code-comparison--no-code"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-no-content">Code Comparison, no content<a class="sniffanchor" href="#Code-Comparison,-no-content"> &sect; </a></h2>
<h2 id="code-comparison--no-content">Code Comparison, no content<a class="sniffanchor" href="#code-comparison--no-content"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<div class="tag-line">Documentation generated on #REDACTED# by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer #VERSION#</a></div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-one-empty-code-element">Code Comparison, one empty code element<a class="sniffanchor" href="#Code-Comparison,-one-empty-code-element"> &sect; </a></h2>
<h2 id="code-comparison--one-empty-code-element">Code Comparison, one empty code element<a class="sniffanchor" href="#code-comparison--one-empty-code-element"> &sect; </a></h2>
<p class="text">This doc has two code elements, but only one of them has a title and actual code.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Comparison,-two-empty-code-elements">Code Comparison, two empty code elements<a class="sniffanchor" href="#Code-Comparison,-two-empty-code-elements"> &sect; </a></h2>
<h2 id="code-comparison--two-empty-code-elements">Code Comparison, two empty code elements<a class="sniffanchor" href="#code-comparison--two-empty-code-elements"> &sect; </a></h2>
<p class="text">This doc has two code elements, but neither of them contain any information.</p>
<div class="tag-line">Documentation generated on #REDACTED# by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer #VERSION#</a></div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Title,-empty">Code Title, empty<a class="sniffanchor" href="#Code-Title,-empty"> &sect; </a></h2>
<h2 id="code-title--empty">Code Title, empty<a class="sniffanchor" href="#code-title--empty"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Code-Title,-missing">Code Title, missing<a class="sniffanchor" href="#Code-Title,-missing"> &sect; </a></h2>
<h2 id="code-title--missing">Code Title, missing<a class="sniffanchor" href="#code-title--missing"> &sect; </a></h2>
<p class="text">This is a standard block.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Documentation-Title-Empty">Documentation Title Empty<a class="sniffanchor" href="#Documentation-Title-Empty"> &sect; </a></h2>
<h2 id="documentation-title-empty">Documentation Title Empty<a class="sniffanchor" href="#documentation-title-empty"> &sect; </a></h2>
<p class="text">The above &quot;documentation&quot; element has an empty title attribute.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Documentation-Title-Missing">Documentation Title Missing<a class="sniffanchor" href="#Documentation-Title-Missing"> &sect; </a></h2>
<h2 id="documentation-title-missing">Documentation Title Missing<a class="sniffanchor" href="#documentation-title-missing"> &sect; </a></h2>
<p class="text">The above &quot;documentation&quot; element is missing the title attribute.</p>
<table class="code-comparison">
<tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Standard-Element,-no-content">Standard Element, no content<a class="sniffanchor" href="#Standard-Element,-no-content"> &sect; </a></h2>
<h2 id="standard-element--no-content">Standard Element, no content<a class="sniffanchor" href="#standard-element--no-content"> &sect; </a></h2>
<table class="code-comparison">
<tr>
<th class="code-comparison-title">Valid: Lorem ipsum dolor sit amet.</th>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="One-Standard-Block,-No-Code">One Standard Block, No Code<a class="sniffanchor" href="#One-Standard-Block,-No-Code"> &sect; </a></h2>
<h2 id="one-standard-block--no-code">One Standard Block, No Code<a class="sniffanchor" href="#one-standard-block--no-code"> &sect; </a></h2>
<p class="text">Documentation contains one standard block and no code comparison.</p>
<div class="tag-line">Documentation generated on #REDACTED# by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer #VERSION#</a></div>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Standard-Element,-blank-line-handling">Standard Element, blank line handling<a class="sniffanchor" href="#Standard-Element,-blank-line-handling"> &sect; </a></h2>
<h2 id="standard-element--blank-line-handling">Standard Element, blank line handling<a class="sniffanchor" href="#standard-element--blank-line-handling"> &sect; </a></h2>
<p class="text">There is a blank line at the start of this standard.</p>
<p class="text">And the above blank line is also deliberate to test part of the logic.</p>
<p class="text">Let&#039;s also end on a blank line to test that too.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Standard-Element,-handling-of-HTML-tags">Standard Element, handling of HTML tags<a class="sniffanchor" href="#Standard-Element,-handling-of-HTML-tags"> &sect; </a></h2>
<h2 id="standard-element--handling-of-html-tags">Standard Element, handling of HTML tags<a class="sniffanchor" href="#standard-element--handling-of-html-tags"> &sect; </a></h2>
<p class="text">The use of <em>tags</em> in standard descriptions is allowed and their handling should be <em>safeguarded</em>.<br/>
Other tags, like &lt;a href=&quot;example.com&quot;&gt;link&lt;/a&gt;, &lt;b&gt;bold&lt;/bold&gt;, &lt;script&gt;&lt;/script&gt; are not allowed and will be encoded for display when the HTML or Markdown report is used.</p>
<div class="tag-line">Documentation generated on #REDACTED# by <a href="https://github.com/PHPCSStandards/PHP_CodeSniffer">PHP_CodeSniffer #VERSION#</a></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Standard-Element,-indentation-should-be-ignored">Standard Element, indentation should be ignored<a class="sniffanchor" href="#Standard-Element,-indentation-should-be-ignored"> &sect; </a></h2>
<h2 id="standard-element--indentation-should-be-ignored">Standard Element, indentation should be ignored<a class="sniffanchor" href="#standard-element--indentation-should-be-ignored"> &sect; </a></h2>
<p class="text">This line has no indentation.<br/>
This line has 4 spaces indentation.<br/>
This line has 8 spaces indentation.<br/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
</head>
<body>
<h1>GeneratorTest Coding Standards</h1>
<h2 id="Standard-Element,-line-wrapping-handling">Standard Element, line wrapping handling<a class="sniffanchor" href="#Standard-Element,-line-wrapping-handling"> &sect; </a></h2>
<h2 id="standard-element--line-wrapping-handling">Standard Element, line wrapping handling<a class="sniffanchor" href="#standard-element--line-wrapping-handling"> &sect; </a></h2>
<p class="text">This line has to be exactly 99 chars to test part of the logic.------------------------------------<br/>
And this line has to be exactly 100 chars.----------------------------------------------------------<br/>
And here we have a line which should start wrapping as it is longer than 100 chars. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pellentesque iaculis enim quis hendrerit. Morbi ultrices in odio pharetra commodo.</p>
Expand Down
Loading

0 comments on commit b2fba89

Please sign in to comment.