This is an automated email from the ASF dual-hosted git repository.

kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-doxia.git


The following commit(s) were added to refs/heads/master by this push:
     new 7a04c033 [DOXIA-722] Optionally create anchors for index entries (used 
in TOC (#180)
7a04c033 is described below

commit 7a04c033456511e1bd4870f03ba768d03158f368
Author: Konrad Windszus <k...@apache.org>
AuthorDate: Fri Jan 5 16:32:11 2024 +0100

    [DOXIA-722] Optionally create anchors for index entries (used in TOC (#180)
    
    macro)
---
 .../org/apache/maven/doxia/index/IndexEntry.java   | 124 +++++++++++-
 .../org/apache/maven/doxia/index/IndexingSink.java | 224 ++++++++++++---------
 .../org/apache/maven/doxia/macro/MacroRequest.java |   4 +-
 .../org/apache/maven/doxia/macro/toc/TocMacro.java |   8 +-
 .../apache/maven/doxia/parser/AbstractParser.java  |  16 ++
 .../java/org/apache/maven/doxia/parser/Parser.java |  17 ++
 .../sink/impl/CreateAnchorsForIndexEntries.java    |  44 ++++
 .../impl/CreateAnchorsForIndexEntriesFactory.java  |  38 ++++
 .../sink/impl/UniqueAnchorNamesValidator.java      |  52 +++++
 .../impl/UniqueAnchorNamesValidatorFactory.java    |  38 ++++
 .../apache/maven/doxia/index/IndexEntryTest.java   |  35 +++-
 .../apache/maven/doxia/macro/toc/TocMacroTest.java |  44 +++-
 .../doxia-module-apt/src/test/resources/test.apt   |   2 +-
 13 files changed, 526 insertions(+), 120 deletions(-)

diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/index/IndexEntry.java 
b/doxia-core/src/main/java/org/apache/maven/doxia/index/IndexEntry.java
index 973f6c16..178e403c 100644
--- a/doxia-core/src/main/java/org/apache/maven/doxia/index/IndexEntry.java
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/index/IndexEntry.java
@@ -19,11 +19,18 @@
 package org.apache.maven.doxia.index;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
+
+import org.apache.maven.doxia.markup.Markup;
+import org.apache.maven.doxia.sink.Sink;
 
 /**
- * <p>IndexEntry class.</p>
+ * Representing the index tree within a document with the most important 
metadata per entry.
+ * Currently this only contains entries for sections, but in the future may be 
extended, therefore it
+ * is recommended to use {@link #getType()} to filter out irrelevant entries.
  *
  * @author <a href="mailto:tryg...@inamo.no";>Trygve Laugst&oslash;l</a>
  */
@@ -38,6 +45,11 @@ public class IndexEntry {
      */
     private String id;
 
+    /**
+     * true if there is already an anchor for this
+     */
+    private boolean hasAnchor;
+
     /**
      * The entry title.
      */
@@ -48,13 +60,50 @@ public class IndexEntry {
      */
     private List<IndexEntry> childEntries = new ArrayList<>();
 
+    public enum Type {
+        /**
+         * Used for unknown types but also for the root entry
+         */
+        UNKNOWN(),
+        SECTION_1(Sink.SECTION_LEVEL_1),
+        SECTION_2(Sink.SECTION_LEVEL_2),
+        SECTION_3(Sink.SECTION_LEVEL_3),
+        SECTION_4(Sink.SECTION_LEVEL_4),
+        SECTION_5(Sink.SECTION_LEVEL_5),
+        SECTION_6(),
+        DEFINED_TERM(),
+        FIGURE(),
+        TABLE();
+
+        private final int sectionLevel;
+
+        Type() {
+            this(-1);
+        }
+
+        Type(int sectionLevel) {
+            this.sectionLevel = sectionLevel;
+        }
+
+        static Type fromSectionLevel(int level) {
+            if (level < Sink.SECTION_LEVEL_1 || level > Sink.SECTION_LEVEL_5) {
+                throw new IllegalArgumentException("Level must be between " + 
Sink.SECTION_LEVEL_1 + " and "
+                        + Sink.SECTION_LEVEL_5 + " but is " + level);
+            }
+            return Arrays.stream(Type.values())
+                    .filter(t -> level == t.sectionLevel)
+                    .findAny()
+                    .orElseThrow(() -> new IllegalStateException("Could not 
find enum for sectionLevel " + level));
+        }
+    };
+
     /**
-     * System-dependent EOL.
+     * The type of the entry, one of the types defined by {@link IndexingSink}
      */
-    private static final String EOL = System.getProperty("line.separator");
+    private final Type type;
 
     /**
-     * Constructor.
+     * Constructor for root entry.
      *
      * @param newId The id. May be null.
      */
@@ -69,12 +118,24 @@ public class IndexEntry {
      * @param newId     The id. May be null.
      */
     public IndexEntry(IndexEntry newParent, String newId) {
+        this(newParent, newId, Type.UNKNOWN);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param newParent The parent. May be null.
+     * @param newId     The id. May be null.
+     * @param
+     */
+    public IndexEntry(IndexEntry newParent, String newId, Type type) {
         this.parent = newParent;
         this.id = newId;
 
         if (parent != null) {
             parent.childEntries.add(this);
         }
+        this.type = type;
     }
 
     /**
@@ -105,6 +166,34 @@ public class IndexEntry {
         this.id = id;
     }
 
+    /**
+     * Returns the type of this entry. Is one of the types defined by {@link 
IndexingSink}.
+     * @return the type of this entry
+     * @since 2.0.0
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /** Set if the entry's id already has an anchor in the underlying document.
+     *
+     * @param hasAnchor {@true} if the id already has an anchor.
+     * @since 2.0.0
+     */
+    public void setAnchor(boolean hasAnchor) {
+        this.hasAnchor = hasAnchor;
+    }
+
+    /**
+     * Returns if the entry's id already has an anchor in the underlying 
document.
+     * @return {@code true} if the id already has an anchor otherwise {@code 
false}.
+     *
+     * @since 2.0.0
+     */
+    public boolean hasAnchor() {
+        return hasAnchor;
+    }
+
     /**
      * Returns the title.
      *
@@ -266,7 +355,7 @@ public class IndexEntry {
             message.append(", title: ").append(title);
         }
 
-        message.append(EOL);
+        message.append(Markup.EOL);
 
         StringBuilder indent = new StringBuilder();
 
@@ -280,4 +369,29 @@ public class IndexEntry {
 
         return message.toString();
     }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(childEntries, hasAnchor, id, parent, title, type);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        IndexEntry other = (IndexEntry) obj;
+        return Objects.equals(childEntries, other.childEntries)
+                && hasAnchor == other.hasAnchor
+                && Objects.equals(id, other.id)
+                && Objects.equals(parent, other.parent)
+                && Objects.equals(title, other.title)
+                && type == other.type;
+    }
 }
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/index/IndexingSink.java 
b/doxia-core/src/main/java/org/apache/maven/doxia/index/IndexingSink.java
index fc2a2d7b..2a3acebd 100644
--- a/doxia-core/src/main/java/org/apache/maven/doxia/index/IndexingSink.java
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/index/IndexingSink.java
@@ -23,49 +23,25 @@ import java.util.Map;
 import java.util.Stack;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.apache.maven.doxia.index.IndexEntry.Type;
+import org.apache.maven.doxia.sink.Sink;
 import org.apache.maven.doxia.sink.SinkEventAttributes;
+import org.apache.maven.doxia.sink.impl.BufferingSinkProxyFactory;
+import 
org.apache.maven.doxia.sink.impl.BufferingSinkProxyFactory.BufferingSink;
 import org.apache.maven.doxia.sink.impl.SinkAdapter;
 import org.apache.maven.doxia.util.DoxiaUtils;
 
 /**
- * A sink implementation for index.
+ * A sink wrapper for populating an index tree for particular elements in a 
document.
+ * Currently this only generates {@link IndexEntry} objects for sections.
  *
  * @author <a href="mailto:tryg...@inamo.no";>Trygve Laugst&oslash;l</a>
  * @author <a href="mailto:vincent.sive...@gmail.com";>Vincent Siveton</a>
  */
-public class IndexingSink extends SinkAdapter {
-    /** Section 1. */
-    private static final int TYPE_SECTION_1 = 1;
-
-    /** Section 2. */
-    private static final int TYPE_SECTION_2 = 2;
-
-    /** Section 3. */
-    private static final int TYPE_SECTION_3 = 3;
-
-    /** Section 4. */
-    private static final int TYPE_SECTION_4 = 4;
-
-    /** Section 5. */
-    private static final int TYPE_SECTION_5 = 5;
-
-    /** Defined term. */
-    private static final int TYPE_DEFINED_TERM = 6;
-
-    /** Figure. */
-    private static final int TYPE_FIGURE = 7;
-
-    /** Table. */
-    private static final int TYPE_TABLE = 8;
-
-    /** Title. */
-    private static final int TITLE = 9;
+public class IndexingSink extends org.apache.maven.doxia.sink.impl.SinkWrapper 
{
 
     /** The current type. */
-    private int type;
-
-    /** The current title. */
-    private String title;
+    private Type type;
 
     /** The stack. */
     private final Stack<IndexEntry> stack;
@@ -76,26 +52,56 @@ public class IndexingSink extends SinkAdapter {
      */
     private final Map<String, AtomicInteger> usedIds;
 
+    private final IndexEntry rootEntry;
+
+    private boolean isComplete;
+    private boolean isTitle;
+    /**
+     * @deprecated legacy constructor, use {@link #IndexingSink(Sink)} with 
{@link SinkAdapter} as argument and call {@link #getRootEntry()} to retrieve 
the index tree afterwards.
+     */
+    @Deprecated
+    public IndexingSink(IndexEntry rootEntry) {
+        this(rootEntry, new SinkAdapter());
+    }
+
+    public IndexingSink(Sink delegate) {
+        this(new IndexEntry("index"), delegate);
+    }
+
     /**
      * Default constructor.
-     *
-     * @param sectionEntry The first index entry.
      */
-    public IndexingSink(IndexEntry sectionEntry) {
+    private IndexingSink(IndexEntry rootEntry, Sink delegate) {
+        super(delegate);
+        this.rootEntry = rootEntry;
         stack = new Stack<>();
-        stack.push(sectionEntry);
+        stack.push(rootEntry);
         usedIds = new HashMap<>();
-        usedIds.put(sectionEntry.getId(), new AtomicInteger());
-        init();
+        usedIds.put(rootEntry.getId(), new AtomicInteger());
+        this.type = Type.UNKNOWN;
     }
 
+    /**
+     * This should only be called once the sink is closed.
+     * Before that the tree might not be complete.
+     * @return the tree of entries starting from the root
+     * @throws IllegalStateException in case the sink was not closed yet
+     */
+    public IndexEntry getRootEntry() {
+        if (!isComplete) {
+            throw new IllegalStateException(
+                    "The sink has not been closed yet, i.e. the index tree is 
not complete yet");
+        }
+        return rootEntry;
+    }
     /**
      * <p>Getter for the field <code>title</code>.</p>
+     * Shortcut for {@link #getRootEntry()} followed by {@link 
IndexEntry#getTitle()}.
      *
      * @return the title
      */
     public String getTitle() {
-        return title;
+        return rootEntry.getTitle();
     }
 
     // ----------------------------------------------------------------------
@@ -104,60 +110,47 @@ public class IndexingSink extends SinkAdapter {
 
     @Override
     public void title(SinkEventAttributes attributes) {
-        this.type = TITLE;
+        isTitle = true;
+        super.title(attributes);
     }
 
     @Override
-    public void section(int level, SinkEventAttributes attributes) {
-        pushNewEntry();
+    public void title_() {
+        isTitle = false;
+        super.title_();
     }
 
     @Override
-    public void section_(int level) {
-        pop();
+    public void section(int level, SinkEventAttributes attributes) {
+        super.section(level, attributes);
+        this.type = IndexEntry.Type.fromSectionLevel(level);
+        pushNewEntry(type);
     }
 
     @Override
-    public void sectionTitle(int level, SinkEventAttributes attributes) {
-        this.type = level;
+    public void section_(int level) {
+        pop();
+        super.section_(level);
     }
 
     @Override
     public void sectionTitle_(int level) {
-        this.type = 0;
+        indexEntryComplete();
+        super.sectionTitle_(level);
     }
 
-    @Override
-    public void title_() {
-        this.type = 0;
-    }
-
-    // public void definedTerm()
-    // {
-    // type = TYPE_DEFINED_TERM;
-    // }
-    //
-    // public void figureCaption()
-    // {
-    // type = TYPE_FIGURE;
-    // }
-    //
-    // public void tableCaption()
-    // {
-    // type = TYPE_TABLE;
-    // }
-
     @Override
     public void text(String text, SinkEventAttributes attributes) {
+        if (isTitle) {
+            rootEntry.setTitle(text);
+            return;
+        }
         switch (this.type) {
-            case TITLE:
-                this.title = text;
-                break;
-            case TYPE_SECTION_1:
-            case TYPE_SECTION_2:
-            case TYPE_SECTION_3:
-            case TYPE_SECTION_4:
-            case TYPE_SECTION_5:
+            case SECTION_1:
+            case SECTION_2:
+            case SECTION_3:
+            case SECTION_4:
+            case SECTION_5:
                 // 
-----------------------------------------------------------------------
                 // Sanitize the id. The most important step is to remove any 
blanks
                 // 
-----------------------------------------------------------------------
@@ -169,16 +162,43 @@ public class IndexingSink extends SinkAdapter {
                 title = title.replaceAll("[\\r\\n]+", "");
                 entry.setTitle(title);
 
-                entry.setId(getUniqueId(DoxiaUtils.encodeId(title)));
-
+                setEntryId(entry, title);
                 break;
-                // Dunno how to handle these yet
-            case TYPE_DEFINED_TERM:
-            case TYPE_FIGURE:
-            case TYPE_TABLE:
+                // Dunno how to handle others yet
             default:
                 break;
         }
+        super.text(text, attributes);
+    }
+
+    @Override
+    public void anchor(String name, SinkEventAttributes attributes) {
+        parseAnchor(name);
+        super.anchor(name, attributes);
+    }
+
+    private boolean parseAnchor(String name) {
+        switch (type) {
+            case SECTION_1:
+            case SECTION_2:
+            case SECTION_3:
+            case SECTION_4:
+            case SECTION_5:
+                IndexEntry entry = stack.lastElement();
+                entry.setAnchor(true);
+                setEntryId(entry, name);
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    private void setEntryId(IndexEntry entry, String id) {
+        if (entry.getId() != null) {
+            usedIds.remove(entry.getId());
+        }
+        entry.setId(getUniqueId(DoxiaUtils.encodeId(id)));
     }
 
     /**
@@ -199,15 +219,34 @@ public class IndexingSink extends SinkAdapter {
         return uniqueId;
     }
 
+    void indexEntryComplete() {
+        this.type = Type.UNKNOWN;
+        // remove buffering sink from pipeline
+        BufferingSink bufferingSink = 
BufferingSinkProxyFactory.castAsBufferingSink(getWrappedSink());
+        setWrappedSink(bufferingSink.getBufferedSink());
+
+        onIndexEntry(stack.peek());
+
+        // flush the buffer afterwards
+        bufferingSink.flush();
+    }
+
     /**
-     * Creates and pushes a new IndexEntry onto the top of this stack.
+     * Called at the beginning of each entry (once all metadata about it is 
collected).
+     * The events for the metadata are buffered and only flushed after this 
method was called.
+     * @param entry the newly collected entry
      */
-    public void pushNewEntry() {
-        IndexEntry entry = new IndexEntry(peek(), "");
+    protected void onIndexEntry(IndexEntry entry) {}
 
+    /**
+     * Creates and pushes a new IndexEntry onto the top of this stack.
+     */
+    private void pushNewEntry(Type type) {
+        IndexEntry entry = new IndexEntry(peek(), "", type);
         entry.setTitle("");
-
         stack.push(entry);
+        // now buffer everything till the next index metadata is complete
+        setWrappedSink(new 
BufferingSinkProxyFactory().createWrapper(getWrappedSink()));
     }
 
     /**
@@ -235,20 +274,9 @@ public class IndexingSink extends SinkAdapter {
         return stack.peek();
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    @Override
     public void close() {
         super.close();
-
-        init();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    protected void init() {
-        this.type = 0;
-        this.title = null;
+        isComplete = true;
     }
 }
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/macro/MacroRequest.java 
b/doxia-core/src/main/java/org/apache/maven/doxia/macro/MacroRequest.java
index 55ed4cf1..7a5b3d63 100644
--- a/doxia-core/src/main/java/org/apache/maven/doxia/macro/MacroRequest.java
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/macro/MacroRequest.java
@@ -44,7 +44,7 @@ public class MacroRequest {
      * <p>Constructor for MacroRequest.</p>
      *
      * @param sourceContent a {@link java.lang.String} object.
-     * @param parser a {@link org.apache.maven.doxia.parser.AbstractParser} 
object.
+     * @param parser a new {@link 
org.apache.maven.doxia.parser.AbstractParser} object acting as secondary parser.
      * @param param a {@link java.util.Map} object.
      * @param basedir a {@link java.io.File} object.
      */
@@ -106,7 +106,7 @@ public class MacroRequest {
     /**
      * <p>getParser.</p>
      *
-     * @return a {@link org.apache.maven.doxia.parser.Parser} object.
+     * @return a {@link org.apache.maven.doxia.parser.Parser} object. This is 
a new secondary parser.
      */
     public Parser getParser() {
         return (Parser) getParameter(PARAM_PARSER);
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/macro/toc/TocMacro.java 
b/doxia-core/src/main/java/org/apache/maven/doxia/macro/toc/TocMacro.java
index 61c9e18c..d07771c2 100644
--- a/doxia-core/src/main/java/org/apache/maven/doxia/macro/toc/TocMacro.java
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/macro/toc/TocMacro.java
@@ -31,6 +31,7 @@ import org.apache.maven.doxia.macro.MacroRequest;
 import org.apache.maven.doxia.parser.ParseException;
 import org.apache.maven.doxia.parser.Parser;
 import org.apache.maven.doxia.sink.Sink;
+import org.apache.maven.doxia.sink.impl.SinkAdapter;
 import org.apache.maven.doxia.util.DoxiaUtils;
 
 /**
@@ -102,15 +103,16 @@ public class TocMacro extends AbstractMacro {
             return;
         }
 
-        IndexEntry index = new IndexEntry("index");
-        IndexingSink tocSink = new IndexingSink(index);
-
+        IndexingSink tocSink = new IndexingSink(new SinkAdapter());
         try {
             parser.parse(new StringReader(source), tocSink);
         } catch (ParseException e) {
             throw new MacroExecutionException(e);
+        } finally {
+            tocSink.close();
         }
 
+        IndexEntry index = tocSink.getRootEntry();
         if (index.getChildEntries().size() > 0) {
             sink.list(getAttributesFromMap(request.getParameters()));
 
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/parser/AbstractParser.java 
b/doxia-core/src/main/java/org/apache/maven/doxia/parser/AbstractParser.java
index a55f1aa1..fcab7bb2 100644
--- a/doxia-core/src/main/java/org/apache/maven/doxia/parser/AbstractParser.java
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/parser/AbstractParser.java
@@ -36,6 +36,7 @@ import org.apache.maven.doxia.macro.MacroRequest;
 import org.apache.maven.doxia.macro.manager.MacroManager;
 import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
 import org.apache.maven.doxia.sink.Sink;
+import org.apache.maven.doxia.sink.impl.CreateAnchorsForIndexEntriesFactory;
 import org.apache.maven.doxia.sink.impl.SinkWrapperFactory;
 import org.apache.maven.doxia.sink.impl.SinkWrapperFactoryComparator;
 
@@ -63,6 +64,8 @@ public abstract class AbstractParser implements Parser {
      */
     private boolean emitComments = true;
 
+    private boolean emitAnchors = false;
+
     private static final String DOXIA_VERSION;
 
     static {
@@ -112,6 +115,16 @@ public abstract class AbstractParser implements Parser {
         return emitComments;
     }
 
+    @Override
+    public boolean isEmitAnchorsForIndexableEntries() {
+        return emitAnchors;
+    }
+
+    @Override
+    public void setEmitAnchorsForIndexableEntries(boolean emitAnchors) {
+        this.emitAnchors = emitAnchors;
+    }
+
     /**
      * Execute a macro on the given sink.
      *
@@ -230,6 +243,9 @@ public abstract class AbstractParser implements Parser {
             
effectiveSinkWrapperFactories.addAll(automaticallyRegisteredSinkWrapperFactories);
         }
         
effectiveSinkWrapperFactories.addAll(manuallyRegisteredSinkWrapperFactories);
+        if (emitAnchors) {
+            effectiveSinkWrapperFactories.add(new 
CreateAnchorsForIndexEntriesFactory());
+        }
         return effectiveSinkWrapperFactories;
     }
 
diff --git a/doxia-core/src/main/java/org/apache/maven/doxia/parser/Parser.java 
b/doxia-core/src/main/java/org/apache/maven/doxia/parser/Parser.java
index da165895..228bc357 100644
--- a/doxia-core/src/main/java/org/apache/maven/doxia/parser/Parser.java
+++ b/doxia-core/src/main/java/org/apache/maven/doxia/parser/Parser.java
@@ -21,6 +21,7 @@ package org.apache.maven.doxia.parser;
 import java.io.Reader;
 import java.util.Collection;
 
+import org.apache.maven.doxia.index.IndexingSink;
 import org.apache.maven.doxia.sink.Sink;
 import org.apache.maven.doxia.sink.impl.SinkWrapperFactory;
 
@@ -99,4 +100,20 @@ public interface Parser {
      * @since 2.0.0
      */
     Collection<SinkWrapperFactory> getSinkWrapperFactories();
+
+    /**
+     * Determines whether to automatically generate anchors for each index 
entry found by {@link IndexingSink} or not.
+     * By default no anchors are generated.
+     *
+     * @param emitAnchors {@code true} to emit anchors otherwise {@code false} 
(the default)
+     * @since 2.0.0
+     */
+    void setEmitAnchorsForIndexableEntries(boolean emitAnchors);
+
+    /**
+     * Returns whether anchors are automatically generated for each index 
entry found by {@link IndexingSink} or not.
+     * @return  {@code true} if anchors are emitted otherwise {@code false}
+     * @since 2.0.0
+     */
+    boolean isEmitAnchorsForIndexableEntries();
 }
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/CreateAnchorsForIndexEntries.java
 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/CreateAnchorsForIndexEntries.java
new file mode 100644
index 00000000..9998b99d
--- /dev/null
+++ 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/CreateAnchorsForIndexEntries.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.doxia.sink.impl;
+
+import org.apache.maven.doxia.index.IndexEntry;
+import org.apache.maven.doxia.index.IndexingSink;
+import org.apache.maven.doxia.macro.toc.TocMacro;
+import org.apache.maven.doxia.sink.Sink;
+
+/**
+ * Sink wrapper which emits anchors for each entry detected by the underlying 
{@link IndexingSink}.
+ * It only creates an anchor if there is no accompanying anchor detected for 
the according entry.
+ * @see TocMacro
+ */
+public class CreateAnchorsForIndexEntries extends IndexingSink {
+
+    public CreateAnchorsForIndexEntries(Sink delegate) {
+        super(delegate);
+    }
+
+    @Override
+    protected void onIndexEntry(IndexEntry entry) {
+        if (!entry.hasAnchor()) {
+            getWrappedSink().anchor(entry.getId());
+            getWrappedSink().anchor_();
+        }
+    }
+}
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/CreateAnchorsForIndexEntriesFactory.java
 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/CreateAnchorsForIndexEntriesFactory.java
new file mode 100644
index 00000000..d6e4ebdc
--- /dev/null
+++ 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/CreateAnchorsForIndexEntriesFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.doxia.sink.impl;
+
+import org.apache.maven.doxia.sink.Sink;
+
+public class CreateAnchorsForIndexEntriesFactory implements SinkWrapperFactory 
{
+
+    public CreateAnchorsForIndexEntriesFactory() {
+        super();
+    }
+
+    @Override
+    public Sink createWrapper(Sink sink) {
+        return new CreateAnchorsForIndexEntries(sink);
+    }
+
+    @Override
+    public int getPriority() {
+        return 0;
+    }
+}
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/UniqueAnchorNamesValidator.java
 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/UniqueAnchorNamesValidator.java
new file mode 100644
index 00000000..b44ab474
--- /dev/null
+++ 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/UniqueAnchorNamesValidator.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.doxia.sink.impl;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.maven.doxia.sink.Sink;
+import org.apache.maven.doxia.sink.SinkEventAttributes;
+
+/**
+ * Validates that each anchor name only appears once per document. Otherwise 
fails with an {@link IllegalStateException}.
+ */
+public class UniqueAnchorNamesValidator extends SinkWrapper {
+
+    private final Set<String> usedAnchorNames;
+
+    public UniqueAnchorNamesValidator(Sink sink) {
+        super(sink);
+        usedAnchorNames = new HashSet<>();
+    }
+
+    @Override
+    public void anchor(String name, SinkEventAttributes attributes) {
+        // assume that other anchor method signature calls this method under 
the hood in all relevant sink
+        // implementations
+        super.anchor(name, attributes);
+        enforceUniqueAnchor(name);
+    }
+
+    private void enforceUniqueAnchor(String name) {
+        if (!usedAnchorNames.add(name)) {
+            throw new IllegalStateException("Anchor name \"" + name + "\" used 
more than once");
+        }
+    }
+}
diff --git 
a/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/UniqueAnchorNamesValidatorFactory.java
 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/UniqueAnchorNamesValidatorFactory.java
new file mode 100644
index 00000000..fa7e6a57
--- /dev/null
+++ 
b/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/UniqueAnchorNamesValidatorFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.doxia.sink.impl;
+
+import javax.inject.Named;
+
+import org.apache.maven.doxia.sink.Sink;
+
+@Named("unique-anchors-validator")
+public class UniqueAnchorNamesValidatorFactory implements SinkWrapperFactory {
+
+    @Override
+    public Sink createWrapper(Sink sink) {
+        return new UniqueAnchorNamesValidator(sink);
+    }
+
+    @Override
+    public int getPriority() {
+        // should come last (after potential preprocessing/modification of 
anchor names)
+        return Integer.MIN_VALUE;
+    }
+}
diff --git 
a/doxia-core/src/test/java/org/apache/maven/doxia/index/IndexEntryTest.java 
b/doxia-core/src/test/java/org/apache/maven/doxia/index/IndexEntryTest.java
index a94a37f8..b2363d90 100644
--- a/doxia-core/src/test/java/org/apache/maven/doxia/index/IndexEntryTest.java
+++ b/doxia-core/src/test/java/org/apache/maven/doxia/index/IndexEntryTest.java
@@ -18,9 +18,12 @@
  */
 package org.apache.maven.doxia.index;
 
+import org.apache.maven.doxia.index.IndexEntry.Type;
+import org.apache.maven.doxia.sink.Sink;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -34,35 +37,42 @@ public class IndexEntryTest {
     public void testIndexEntry() {
         IndexEntry root = new IndexEntry(null);
 
-        assertIndexEntry(root, null, 0, null, null);
+        assertIndexEntry(root, Type.UNKNOWN, null, 0, null, null);
 
         // 
-----------------------------------------------------------------------
         // Chapter 1
         // 
-----------------------------------------------------------------------
 
-        IndexEntry chapter1 = new IndexEntry(root, "chapter-1");
+        IndexEntry chapter1 = new IndexEntry(root, "chapter-1", 
Type.SECTION_1);
 
-        assertIndexEntry(root, null, 1, null, null);
+        assertIndexEntry(root, Type.UNKNOWN, null, 1, null, null);
 
-        assertIndexEntry(chapter1, root, 0, null, null);
+        assertIndexEntry(chapter1, Type.SECTION_1, root, 0, null, null);
 
         // 
-----------------------------------------------------------------------
         // Chapter 2
         // 
-----------------------------------------------------------------------
 
-        IndexEntry chapter2 = new IndexEntry(root, "chapter-2");
+        IndexEntry chapter2 = new IndexEntry(root, "chapter-2", 
Type.SECTION_1);
 
-        assertIndexEntry(root, null, 2, null, null);
+        assertIndexEntry(root, Type.UNKNOWN, null, 2, null, null);
 
-        assertIndexEntry(chapter1, root, 0, null, chapter2);
-        assertIndexEntry(chapter2, root, 0, chapter1, null);
+        assertIndexEntry(chapter1, Type.SECTION_1, root, 0, null, chapter2);
+        assertIndexEntry(chapter2, Type.SECTION_1, root, 0, chapter1, null);
 
         chapter2.setTitle("Title 2");
         assertTrue(chapter2.toString().contains("Title 2"));
     }
 
     private void assertIndexEntry(
-            IndexEntry entry, IndexEntry parent, int childCount, IndexEntry 
prevEntry, IndexEntry nextEntry) {
+            IndexEntry entry,
+            Type type,
+            IndexEntry parent,
+            int childCount,
+            IndexEntry prevEntry,
+            IndexEntry nextEntry) {
+        assertEquals(type, entry.getType());
+
         assertEquals(parent, entry.getParent());
 
         assertEquals(childCount, entry.getChildEntries().size());
@@ -71,4 +81,11 @@ public class IndexEntryTest {
 
         assertEquals(nextEntry, entry.getNextEntry());
     }
+
+    @Test
+    public void testTypeFromSectionLevel() {
+        assertThrows(IllegalArgumentException.class, () -> 
Type.fromSectionLevel(0));
+        assertEquals(Type.SECTION_3, 
Type.fromSectionLevel(Sink.SECTION_LEVEL_3));
+        assertThrows(IllegalArgumentException.class, () -> 
Type.fromSectionLevel(7));
+    }
 }
diff --git 
a/doxia-core/src/test/java/org/apache/maven/doxia/macro/toc/TocMacroTest.java 
b/doxia-core/src/test/java/org/apache/maven/doxia/macro/toc/TocMacroTest.java
index 3c350e3f..a8cc291f 100644
--- 
a/doxia-core/src/test/java/org/apache/maven/doxia/macro/toc/TocMacroTest.java
+++ 
b/doxia-core/src/test/java/org/apache/maven/doxia/macro/toc/TocMacroTest.java
@@ -26,14 +26,21 @@ import java.util.Map;
 
 import org.apache.maven.doxia.macro.MacroExecutionException;
 import org.apache.maven.doxia.macro.MacroRequest;
+import org.apache.maven.doxia.markup.Markup;
+import org.apache.maven.doxia.parser.AbstractParserTest;
+import org.apache.maven.doxia.parser.ParseException;
 import org.apache.maven.doxia.parser.Xhtml5BaseParser;
+import org.apache.maven.doxia.sink.Sink;
+import org.apache.maven.doxia.sink.impl.CreateAnchorsForIndexEntriesFactory;
 import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
 import org.apache.maven.doxia.sink.impl.SinkEventElement;
 import org.apache.maven.doxia.sink.impl.SinkEventTestingSink;
 import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink;
 import org.junit.jupiter.api.Test;
 
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Test toc macro.
@@ -96,8 +103,10 @@ public class TocMacroTest {
         assertEquals("list_", (it.next()).getName());
         assertFalse(it.hasNext());
 
-        // test parameters
+        // no wrapper factories should be registered by default
+        assertEquals(0, parser.getSinkWrapperFactories().size());
 
+        // test parameters
         parser = new Xhtml5BaseParser();
         macroParameters.put("section", "2");
         macroParameters.put("fromDepth", "1");
@@ -162,4 +171,35 @@ public class TocMacroTest {
         assertTrue(out.toString().contains("<a href=\"#h12\">h12</a>"));
         assertTrue(out.toString().contains("<a href=\"#h2\">h2</a>"));
     }
+
+    @Test
+    public void testGenerateAnchors() throws ParseException, 
MacroExecutionException {
+        String sourceContent = "<h1>1 Headline</h1>";
+        File basedir = new File("");
+        Xhtml5BaseParser parser = new Xhtml5BaseParser();
+        MacroRequest request = new MacroRequest(sourceContent, parser, new 
HashMap<>(), basedir);
+        TocMacro macro = new TocMacro();
+        SinkEventTestingSink sink = new SinkEventTestingSink();
+
+        macro.execute(sink, request);
+        parser.parse(sourceContent, sink);
+
+        Iterator<SinkEventElement> it = sink.getEventList().iterator();
+        AbstractParserTest.assertSinkStartsWith(it, "list", "listItem");
+        SinkEventElement link = it.next();
+        assertEquals("link", link.getName());
+        String actualLinkTarget = (String) link.getArgs()[0];
+        AbstractParserTest.assertSinkEquals(it.next(), "text", "1 Headline", 
null);
+        AbstractParserTest.assertSinkEquals(
+                it, "link_", "listItem_", "list_", "section1", 
"sectionTitle1", "text", "sectionTitle1_");
+
+        // check html output as well (without the actual TOC)
+        StringWriter out = new StringWriter();
+        Sink sink2 = new Xhtml5BaseSink(out);
+        parser.addSinkWrapperFactory(new 
CreateAnchorsForIndexEntriesFactory());
+        parser.parse(sourceContent, sink2);
+        assertEquals(
+                "<section><a id=\"" + actualLinkTarget.substring(1) + 
"\"></a>" + Markup.EOL + "<h1>1 Headline</h1>",
+                out.toString());
+    }
 }
diff --git a/doxia-modules/doxia-module-apt/src/test/resources/test.apt 
b/doxia-modules/doxia-module-apt/src/test/resources/test.apt
index 0fcb00cf..87bf17a8 100644
--- a/doxia-modules/doxia-module-apt/src/test/resources/test.apt
+++ b/doxia-modules/doxia-module-apt/src/test/resources/test.apt
@@ -15,7 +15,7 @@ Section title
 
 * Sub-section title
 
-** Sub-sub-section title
+** {Sub-sub-section title}
 
 *** Sub-sub-sub-section title
 


Reply via email to