Skip to content

Fix section ID deduplication by looping through the whole node tree#1322

Merged
jaapio merged 1 commit intophpDocumentor:mainfrom
wouterj:section-id-deduplication
Mar 22, 2026
Merged

Fix section ID deduplication by looping through the whole node tree#1322
jaapio merged 1 commit intophpDocumentor:mainfrom
wouterj:section-id-deduplication

Conversation

@wouterj
Copy link
Copy Markdown
Contributor

@wouterj wouterj commented Mar 21, 2026

The compiler pass resolving conflicts in section IDs only looped one level deep through the nodes. This means only the "main" document section was checked for conflicts, instead of all subsections of the document.


There is another problem with section IDs that I'm investigating, which is related to the MoveAnchorTransformer when using explicit hyperlink targets. The AnchorNode is copied into the section node by this transformer, but not removed from the original location. Leading to duplicate anchors like:

        ...
        <a id="something-different"></a>
    </div>
    <div class="section" id="another-subtitle">
        <a id="something-different"></a>
        <h2>
            Another Subtitle
        </h2>
        ...

But as that looks a bit more complex, I'll open a separate PR once I find the fix for this issue.

Comment on lines -55 to -70
if ($node instanceof AnchorNode) {
// override implicit section reference if an anchor precedes the section
$key = key($nodes);
$section = next($nodes);
if (!$section instanceof SectionNode) {
prev($nodes);
continue;
}

$section->getTitle()->setId($node->getValue());
if ($key !== null) {
$document = $document->removeNode($key);
}

continue;
}
Copy link
Copy Markdown
Contributor Author

@wouterj wouterj Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is redundant: Explicit anchors don't override implicit section IDs. They add additional section IDs.

    <div class="section" id="another-subtitle">
        <a id="something-different"></a>

The menu logic can keep using the implicit references, reducing complexity in this compiler pass.

@wouterj wouterj force-pushed the section-id-deduplication branch from 4d67d2b to 7d4cd7b Compare March 22, 2026 13:37
@wouterj wouterj force-pushed the section-id-deduplication branch from 7d4cd7b to 4c92985 Compare March 22, 2026 13:45
@jaapio jaapio merged commit be8d211 into phpDocumentor:main Mar 22, 2026
58 checks passed
@jaapio
Copy link
Copy Markdown
Member

jaapio commented Mar 22, 2026

Thanks, for this improvement. Really nice to see you are making progress.

@jaapio
Copy link
Copy Markdown
Member

jaapio commented Mar 22, 2026

Regarding nodes that are not removed. Is this at the root level of documents? I remember that I found something was wrong there.
I need to check if I fixed that already.

@wouterj wouterj deleted the section-id-deduplication branch March 22, 2026 20:06
@wouterj
Copy link
Copy Markdown
Contributor Author

wouterj commented Mar 22, 2026

Regarding nodes that are not removed. Is this at the root level of documents? I remember that I found something was wrong there.

If you add this patch to the test fixture introduced in this PR, you can observe it:

diff --git a/tests/Functional/tests/titles-ids/titles-ids.html b/tests/Functional/tests/titles-ids/titles-ids.html
index 78d4e577..e98e5038 100644
--- a/tests/Functional/tests/titles-ids/titles-ids.html
+++ b/tests/Functional/tests/titles-ids/titles-ids.html
@@ -24,4 +24,18 @@
         </h2>
         <p>This also works for titles with markup.</p>
     </div>
+    <div class="section" id="another-subtitle">
+        <a id="something-different"></a>
+        <h2>
+            Another Subtitle
+        </h2>
+        <p>You can override subtitle IDs by placing an anchor before the section.</p>
+        <div class="section" id="subsubtitle">
+            <a id="something"></a>
+            <h3>
+                Subsubtitle
+            </h3>
+            <p>This works on all levels.</p>
+        </div>
+    </div>
 </div>
diff --git a/tests/Functional/tests/titles-ids/titles-ids.rst b/tests/Functional/tests/titles-ids/titles-ids.rst
index 33f2c7ca..244c8bf5 100644
--- a/tests/Functional/tests/titles-ids/titles-ids.rst
+++ b/tests/Functional/tests/titles-ids/titles-ids.rst
@@ -16,3 +16,17 @@ Other ``Subtitle``
 ------------------
 
 This also works for titles with markup.
+
+.. _something-different:
+
+Another Subtitle
+----------------
+
+You can override subtitle IDs by placing an anchor before the section.
+
+.. _something:
+
+Subsubtitle
+~~~~~~~~~~~
+
+This works on all levels.

This makes the test fail with:

--- Expected
+++ Actual
@@ @@
             Other <code>Subtitle</code>
         </h2>
         <p>This also works for titles with markup.</p>
+        <a id="something-different"></a>
     </div>
     <div class="section" id="another-subtitle">
         <a id="something-different"></a>
@@ @@
             Another Subtitle
         </h2>
         <p>You can override subtitle IDs by placing an anchor before the section.</p>
+        <a id="something"></a>
         <div class="section" id="subsubtitle">
             <a id="something"></a>
             <h3>

But I somehow can't reproduce this in the Symfony docs-builder test suite (where we test explicit anchors as well). I've traced it back to

$shadowNode->getParent()?->removeChild($node);
Somehow TreeNode::removeNode() doesn't appear to remove the node in the testsuite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants