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

pkarwasz pushed a commit to branch feature/asciidoctor-converter
in repository https://gitbox.apache.org/repos/asf/logging-log4j-tools.git

commit d022f0469d7a1e5057688164089863ac3664e55e
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Wed Jan 31 23:18:27 2024 +0100

    Add Javadoc to Asciidoc converter
    
    We add a converter between Javadoc and Asciidoc that converts:
    
     * from a 
[`DocCommentTree`](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/com/sun/source/doctree/DocCommentTree.html),
       provided by the `javac` compiler or `javadoc` tool,
     * to an AsciiDoctorJ 
[`Document`](https://javadoc.io/static/org.asciidoctor/asciidoctorj/3.0.0-alpha.2/org/asciidoctor/ast/Document.html).
    
    We provide a primitive implementation of the AsciiDoctorJ API that
    converts the AST back into a text document.
---
 log4j-docgen/pom.xml                               |  28 ++
 .../log4j/docgen/processor/AsciidocConverter.java  | 382 +++++++++++++++++++++
 .../log4j/docgen/processor/internal/BlockImpl.java |  73 ++++
 .../log4j/docgen/processor/internal/CellImpl.java  | 113 ++++++
 .../docgen/processor/internal/ContentNodeImpl.java | 190 ++++++++++
 .../docgen/processor/internal/DocumentImpl.java    | 124 +++++++
 .../log4j/docgen/processor/internal/ListImpl.java  |  96 ++++++
 .../docgen/processor/internal/ListItemImpl.java    |  74 ++++
 .../log4j/docgen/processor/internal/RowImpl.java   |  32 ++
 .../docgen/processor/internal/SectionImpl.java     | 108 ++++++
 .../processor/internal/StructuralNodeImpl.java     | 158 +++++++++
 .../log4j/docgen/processor/internal/TableImpl.java | 120 +++++++
 .../src/test/it/example/JavadocExample.java        | 100 ++++++
 .../docgen/processor/AsciidocConverterTest.java    | 133 +++++++
 .../expected/processor/JavadocExample.adoc         | 107 ++++++
 log4j-tools-parent/pom.xml                         |  16 +
 16 files changed, 1854 insertions(+)

diff --git a/log4j-docgen/pom.xml b/log4j-docgen/pom.xml
index d8d735f..11fd992 100644
--- a/log4j-docgen/pom.xml
+++ b/log4j-docgen/pom.xml
@@ -27,6 +27,7 @@
   <artifactId>log4j-docgen</artifactId>
 
   <properties>
+    <maven.compiler.release>17</maven.compiler.release>
     <bnd.baseline.fail.on.missing>false</bnd.baseline.fail.on.missing>
   </properties>
 
@@ -38,11 +39,38 @@
       <scope>provided</scope>
     </dependency>
 
+    <dependency>
+      <groupId>org.asciidoctor</groupId>
+      <artifactId>asciidoctorj-api</artifactId>
+      <version>3.0.0-alpha.2</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>org.freemarker</groupId>
       <artifactId>freemarker</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-api</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-plugins</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-api</artifactId>
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocConverter.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocConverter.java
new file mode 100644
index 0000000..41772c4
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocConverter.java
@@ -0,0 +1,382 @@
+/*
+ * 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.logging.log4j.docgen.processor;
+
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.DocTreeVisitor;
+import com.sun.source.doctree.EndElementTree;
+import com.sun.source.doctree.LiteralTree;
+import com.sun.source.doctree.StartElementTree;
+import com.sun.source.doctree.TextTree;
+import com.sun.source.util.DocTrees;
+import com.sun.source.util.SimpleDocTreeVisitor;
+import java.util.ArrayList;
+import java.util.EmptyStackException;
+import java.util.function.Function;
+import javax.lang.model.element.Element;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.docgen.processor.internal.BlockImpl;
+import org.apache.logging.log4j.docgen.processor.internal.CellImpl;
+import org.apache.logging.log4j.docgen.processor.internal.DocumentImpl;
+import org.apache.logging.log4j.docgen.processor.internal.ListImpl;
+import org.apache.logging.log4j.docgen.processor.internal.ListItemImpl;
+import org.apache.logging.log4j.docgen.processor.internal.RowImpl;
+import org.apache.logging.log4j.docgen.processor.internal.SectionImpl;
+import org.apache.logging.log4j.docgen.processor.internal.TableImpl;
+import org.asciidoctor.ast.Block;
+import org.asciidoctor.ast.Cell;
+import org.asciidoctor.ast.Document;
+import org.asciidoctor.ast.List;
+import org.asciidoctor.ast.ListItem;
+import org.asciidoctor.ast.Row;
+import org.asciidoctor.ast.Section;
+import org.asciidoctor.ast.StructuralNode;
+import org.asciidoctor.ast.Table;
+
+/**
+ * Converts a {@link DocCommentTree} into AsciiDoc text.
+ */
+class AsciidocConverter {
+
+    private static final DocTreeVisitor<Void, Data> TO_ASCIIDOC_VISITOR = new 
ToAsciiDocVisitor();
+
+    private final DocTrees docTrees;
+
+    AsciidocConverter(final DocTrees docTrees) {
+        this.docTrees = docTrees;
+    }
+
+    public String toAsciiDoc(final Element element) {
+        final DocCommentTree tree = docTrees.getDocCommentTree(element);
+        return tree != null ? toAsciiDoc(tree, 
element.getSimpleName().toString()) : null;
+    }
+
+    public String toAsciiDoc(final DocCommentTree tree, final String title) {
+        final Data data = new Data(title);
+        tree.accept(TO_ASCIIDOC_VISITOR, data);
+        return data.getDocument().convert();
+    }
+
+    private static class ToAsciiDocVisitor extends SimpleDocTreeVisitor<Void, 
Data> {
+
+        @Override
+        public Void visitDocComment(final DocCommentTree node, final Data 
data) {
+            // Summary block wrapped in a new paragraph.
+            for (final DocTree docTree : node.getFirstSentence()) {
+                docTree.accept(this, data);
+            }
+            data.newParagraph();
+            // Body
+            for (final DocTree docTree : node.getBody()) {
+                docTree.accept(this, data);
+            }
+            // Flushes the last paragraph
+            data.newParagraph();
+            return super.visitDocComment(node, data);
+        }
+
+        @Override
+        public Void visitStartElement(final StartElementTree node, final Data 
data) {
+            final String elementName = node.getName().toString();
+            switch (elementName) {
+                case "p":
+                    data.newParagraph();
+                    break;
+                case "ol":
+                    // Nested list without a first paragraph
+                    if (data.getCurrentNode() instanceof ListItem) {
+                        data.newParagraph();
+                    }
+                    
data.pushChildNode(ListImpl::new).setContext(ListImpl.ORDERED_LIST_CONTEXT);
+                    break;
+                case "ul":
+                    // Nested list without a first paragraph
+                    if (data.getCurrentNode() instanceof ListItem) {
+                        data.newParagraph();
+                    }
+                    
data.pushChildNode(ListImpl::new).setContext(ListImpl.UNORDERED_LIST_CONTEXT);
+                    break;
+                case "li":
+                    if (!(data.getCurrentNode() instanceof List)) {
+                        throw new IllegalArgumentException("A <li> tag must be 
a child of a <ol> or <ul> tag.");
+                    }
+                    data.pushChildNode(ListItemImpl::new);
+                    break;
+                case "h1":
+                case "h2":
+                case "h3":
+                case "h4":
+                case "h5":
+                case "h6":
+                    // Flush the current paragraph
+                    data.newParagraph();
+                    StructuralNode currentNode;
+                    // Remove other types of nodes from stack
+                    while ((currentNode = data.getCurrentNode()) != null
+                            && !(currentNode instanceof Section || currentNode 
instanceof Document)) {
+                        data.popNode();
+                    }
+                    break;
+                case "table":
+                    data.pushChildNode(TableImpl::new);
+                    break;
+                case "tr":
+                    break;
+                case "th":
+                    
data.pushChildNode(CellImpl::new).setContext(CellImpl.HEADER_CONTEXT);
+                    break;
+                case "td":
+                    data.pushChildNode(CellImpl::new);
+                    break;
+                case "pre":
+                    data.newParagraph();
+                    final Block currentParagraph = data.getCurrentParagraph();
+                    currentParagraph.setContext(BlockImpl.LISTING_CONTEXT);
+                    currentParagraph.setStyle(BlockImpl.SOURCE_STYLE);
+                    break;
+                default:
+            }
+            return super.visitStartElement(node, data);
+        }
+
+        @Override
+        public Void visitEndElement(final EndElementTree node, final Data 
data) {
+            final String elementName = node.getName().toString();
+            switch (elementName) {
+                case "p":
+                    // Ignore closing tags.
+                    break;
+                case "ol":
+                case "ul":
+                case "li":
+                case "table":
+                case "th":
+                case "td":
+                    data.popNode();
+                    break;
+                case "h1":
+                case "h2":
+                case "h3":
+                case "h4":
+                case "h5":
+                case "h6":
+                    // Only flush the current line
+                    if (!data.getCurrentLine().isEmpty()) {
+                        data.newLine();
+                    }
+                    // The current paragraph contains the title
+                    // We retrieve the text and empty the paragraph
+                    final Block currentParagraph = data.getCurrentParagraph();
+                    final String title = 
StringUtils.normalizeSpace(currentParagraph.convert());
+                    currentParagraph.setLines(new ArrayList<>());
+
+                    // There should be no <h1> tags
+                    final int newLevel = "h1".equals(elementName) ? 2 : 
elementName.charAt(1) - '0';
+                    data.setCurrentSectionLevel(newLevel);
+                    data.getCurrentNode().setTitle(title);
+                    break;
+                case "pre":
+                    data.newParagraph();
+                    break;
+                case "tr":
+                    // We group the new cells into a row
+                    final Table table = (Table) data.getCurrentNode();
+                    final java.util.List<StructuralNode> cells = 
table.getBlocks();
+                    // First index of the row
+                    int idx = 0;
+                    for (final Row row : table.getHeader()) {
+                        idx += row.getCells().size();
+                    }
+                    for (final Row row : table.getBody()) {
+                        idx += row.getCells().size();
+                    }
+                    final Row row = new RowImpl();
+                    String context = CellImpl.BODY_CONTEXT;
+                    for (int i = idx; i < table.getBlocks().size(); i++) {
+                        final StructuralNode cell = cells.get(i);
+                        context = cell.getContext();
+                        row.getCells().add((Cell) cell);
+                    }
+                    if (CellImpl.HEADER_CONTEXT.equals(context)) {
+                        table.getHeader().add(row);
+                    } else {
+                        table.getBody().add(row);
+                    }
+                    break;
+                default:
+            }
+            return super.visitEndElement(node, data);
+        }
+
+        @Override
+        public Void visitLiteral(final LiteralTree node, final Data data) {
+            if (node.getKind() == DocTree.Kind.CODE) {
+                if (!data.getCurrentLine().isEmpty()) {
+                    data.append(" ");
+                }
+                data.append("`").append(node.getBody().getBody()).append("`");
+            } else {
+                node.getBody().accept(this, data);
+            }
+            return super.visitLiteral(node, data);
+        }
+
+        @Override
+        public Void visitText(final TextTree node, final Data data) {
+            final Block currentParagraph = data.getCurrentParagraph();
+            if 
(BlockImpl.PARAGRAPH_CONTEXT.equals(currentParagraph.getContext())) {
+                appendSentences(node.getBody(), data);
+            } else {
+                data.append(node.getBody());
+            }
+            return super.visitText(node, data);
+        }
+
+        private static void appendSentences(final String text, final Data 
data) {
+            final String body = StringUtils.normalizeSpace(text);
+            final String[] sentences = body.split("(?<=[.!?])", -1);
+            // Full sentences
+            for (int i = 0; i < sentences.length - 1; i++) {
+                data.appendWords(sentences[i].strip());
+                data.newLine();
+            }
+            // Partial sentence
+            data.appendWords(sentences[sentences.length - 1]);
+        }
+    }
+
+    private static class Data {
+        private final Document document;
+        private int currentSectionLevel;
+        private StructuralNode currentNode;
+        // not attached to the current node
+        private Block currentParagraph;
+        private final StringBuilder currentLine;
+
+        public Data(final String title) {
+            document = new DocumentImpl(title);
+            currentSectionLevel = 1;
+            currentNode = document;
+            currentParagraph = new BlockImpl(currentNode);
+            currentLine = new StringBuilder();
+        }
+
+        public void newLine() {
+            // Remove trailing space
+            final String line = currentLine.toString().stripTrailing();
+            // Ignore leading empty lines
+            if (!currentParagraph.getLines().isEmpty() || !line.isEmpty()) {
+                currentParagraph.getLines().add(line);
+            }
+            currentLine.setLength(0);
+        }
+
+        public Data append(final String text) {
+            final String[] lines = text.split("\r?\n", -1);
+            for (int i = 0; i < lines.length; i++) {
+                currentLine.append(lines[i]);
+                if (i != lines.length - 1) {
+                    newLine();
+                }
+            }
+            return this;
+        }
+
+        public void appendWords(final String words) {
+            // Separate text from previous words
+            if (!currentLine.isEmpty() && !words.isBlank()) {
+                currentLine.append(" ");
+            }
+            currentLine.append(words);
+        }
+
+        public void newParagraph() {
+            newLine();
+            final java.util.List<String> lines = currentParagraph.getLines();
+            // Remove trailing empty lines
+            for (int i = lines.size() - 1; i >= 0; i--) {
+                if (lines.get(i).isEmpty()) {
+                    lines.remove(i);
+                }
+            }
+            if (!currentParagraph.getLines().isEmpty()) {
+                currentNode.append(currentParagraph);
+                currentParagraph = new BlockImpl(currentNode);
+            }
+        }
+
+        public StructuralNode getCurrentNode() {
+            return currentNode;
+        }
+
+        public Block getCurrentParagraph() {
+            return currentParagraph;
+        }
+
+        public StringBuilder getCurrentLine() {
+            return currentLine;
+        }
+
+        public Document getDocument() {
+            return document;
+        }
+
+        public void setCurrentSectionLevel(final int sectionLevel) {
+            while (sectionLevel < currentSectionLevel) {
+                popNode();
+                currentSectionLevel--;
+            }
+            while (sectionLevel > currentSectionLevel) {
+                pushChildNode(SectionImpl::new);
+                currentSectionLevel++;
+            }
+        }
+
+        /**
+         * Creates and appends a new child to the current node.
+         * @param supplier a function to create a new node that takes its 
parent node a parameter.
+         */
+        public StructuralNode pushChildNode(final Function<? super 
StructuralNode, ? extends StructuralNode> supplier) {
+            // Flushes the current paragraph
+            newParagraph();
+
+            final StructuralNode child = supplier.apply(currentNode);
+            // Creates a new current paragraph
+            currentParagraph = new BlockImpl(child);
+
+            currentNode.append(child);
+            return currentNode = child;
+        }
+
+        public StructuralNode popNode() {
+            final StructuralNode currentNode = this.currentNode;
+            // Flushes the current paragraph
+            newParagraph();
+
+            final StructuralNode parent = (StructuralNode) 
currentNode.getParent();
+            if (parent == null) {
+                throw new EmptyStackException();
+            }
+            // Creates a new current paragraph
+            currentParagraph = new BlockImpl(parent);
+
+            this.currentNode = parent;
+            return currentNode;
+        }
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/BlockImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/BlockImpl.java
new file mode 100644
index 0000000..a2dbfd8
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/BlockImpl.java
@@ -0,0 +1,73 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.asciidoctor.ast.Block;
+import org.asciidoctor.ast.ContentNode;
+
+public class BlockImpl extends StructuralNodeImpl implements Block {
+
+    public static final String PARAGRAPH_CONTEXT = "paragraph";
+    public static final String LISTING_CONTEXT = "listing";
+    public static final String SOURCE_STYLE = "source";
+
+    private List<String> lines = new ArrayList<>();
+
+    public BlockImpl(final ContentNode parent) {
+        super(parent);
+        setContext(PARAGRAPH_CONTEXT);
+    }
+
+    @Override
+    public void formatTo(final StringBuilder buffer) {
+        if (getStyle() != null) {
+            buffer.append('[').append(getStyle()).append("]\n");
+        }
+        if (LISTING_CONTEXT.equals(getContext())) {
+            buffer.append("----\n");
+        }
+        lines.forEach(line -> buffer.append(line).append('\n'));
+        if (LISTING_CONTEXT.equals(getContext())) {
+            buffer.append("----\n");
+        }
+    }
+
+    @Override
+    public List<String> getLines() {
+        return lines;
+    }
+
+    @Override
+    public void setLines(final List<String> lines) {
+        this.lines = lines;
+    }
+
+    //
+    // All methods below this line are not implemented.
+    //
+    @Override
+    public String getSource() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setSource(final String source) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/CellImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/CellImpl.java
new file mode 100644
index 0000000..a28c9a9
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/CellImpl.java
@@ -0,0 +1,113 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.asciidoctor.ast.Cell;
+import org.asciidoctor.ast.Column;
+import org.asciidoctor.ast.ContentNode;
+import org.asciidoctor.ast.Document;
+import org.asciidoctor.ast.Table;
+
+public class CellImpl extends StructuralNodeImpl implements Cell {
+
+    public static final String HEADER_CONTEXT = "header";
+    public static final String BODY_CONTEXT = "body";
+
+    public CellImpl(final ContentNode parent) {
+        super(parent);
+        setContext(BODY_CONTEXT);
+    }
+
+    @Override
+    public void formatTo(final StringBuilder buffer) {
+        if (getBlocks().size() > 1) {
+            buffer.append('a');
+        }
+        buffer.append("| ");
+        getBlocks().forEach(node -> {
+            if (node instanceof final StringBuilderFormattable formattable) {
+                formattable.formatTo(buffer);
+            } else {
+                buffer.append(node.convert());
+            }
+        });
+    }
+
+    //
+    // All methods below this line are not implemented.
+    //
+    @Override
+    public Column getColumn() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getColspan() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getRowspan() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getText() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getSource() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setSource(final String source) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Table.HorizontalAlignment getHorizontalAlignment() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setHorizontalAlignment(final Table.HorizontalAlignment halign) 
{
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Table.VerticalAlignment getVerticalAlignment() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setVerticalAlignment(final Table.VerticalAlignment valign) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Document getInnerDocument() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setInnerDocument(final Document document) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ContentNodeImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ContentNodeImpl.java
new file mode 100644
index 0000000..63e2b91
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ContentNodeImpl.java
@@ -0,0 +1,190 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import java.util.List;
+import java.util.Map;
+import org.asciidoctor.ast.ContentNode;
+import org.asciidoctor.ast.Document;
+
+public abstract class ContentNodeImpl implements ContentNode {
+
+    private final ContentNode parent;
+    private String context;
+
+    protected ContentNodeImpl(final ContentNode parent) {
+        this.parent = parent;
+    }
+
+    @Override
+    public ContentNode getParent() {
+        return parent;
+    }
+
+    @Override
+    public String getContext() {
+        return context;
+    }
+
+    @Override
+    public void setContext(final String context) {
+        this.context = context;
+    }
+
+    @Override
+    public Document getDocument() {
+        return parent != null ? parent.getDocument() : null;
+    }
+
+    //
+    // All methods below this line are not implemented.
+    //
+    @Override
+    public String getId() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setId(final String id) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getNodeName() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Map<String, Object> getAttributes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getAttribute(final Object name, final Object defaultValue, 
final boolean inherit) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getAttribute(final Object name, final Object defaultValue) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getAttribute(final Object name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasAttribute(final Object name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasAttribute(final Object name, final boolean inherited) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isAttribute(final Object name, final Object expected) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isAttribute(final Object name, final Object expected, final 
boolean inherit) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean setAttribute(final Object name, final Object value, final 
boolean overwrite) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isOption(final Object name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isRole() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasRole(final String role) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getRole() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getRoles() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void addRole(final String role) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeRole(final String role) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isReftext() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getReftext() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String iconUri(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String mediaUri(final String target) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String imageUri(final String targetImage) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String imageUri(final String targetImage, final String assetDirKey) 
{
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String readAsset(final String path, final Map<Object, Object> opts) 
{
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String normalizeWebPath(final String path, final String start, 
final boolean preserveUriTarget) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/DocumentImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/DocumentImpl.java
new file mode 100644
index 0000000..c75a347
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/DocumentImpl.java
@@ -0,0 +1,124 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.asciidoctor.ast.Author;
+import org.asciidoctor.ast.Catalog;
+import org.asciidoctor.ast.Document;
+import org.asciidoctor.ast.RevisionInfo;
+import org.asciidoctor.ast.StructuralNode;
+import org.asciidoctor.ast.Title;
+
+public class DocumentImpl extends StructuralNodeImpl implements Document {
+
+    public DocumentImpl(final String title) {
+        super(null);
+        setTitle(title);
+    }
+
+    @Override
+    public void formatTo(final StringBuilder buffer) {
+        if (!StringUtils.isBlank(getTitle())) {
+            buffer.append("= ").append(getTitle()).append("\n\n");
+        }
+        boolean first = true;
+        for (final StructuralNode node : getBlocks()) {
+            if (!first) {
+                buffer.append('\n');
+            } else {
+                first = false;
+            }
+            if (node instanceof final StringBuilderFormattable formattable) {
+                formattable.formatTo(buffer);
+            } else {
+                buffer.append(node.convert());
+            }
+        }
+    }
+
+    @Override
+    public String getDoctitle() {
+        return getTitle();
+    }
+
+    //
+    // All methods below this line are not implemented.
+    //
+    @Override
+    public Title getStructuredDoctitle() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<Author> getAuthors() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getSource() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getSourceLines() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isBasebackend(final String backend) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Map<Object, Object> getOptions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getAndIncrementCounter(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getAndIncrementCounter(final String name, final int 
initialValue) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isSourcemap() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setSourcemap(final boolean state) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Catalog getCatalog() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public RevisionInfo getRevisionInfo() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ListImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ListImpl.java
new file mode 100644
index 0000000..d802825
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ListImpl.java
@@ -0,0 +1,96 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import java.util.Objects;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.asciidoctor.ast.ContentNode;
+import org.asciidoctor.ast.List;
+import org.asciidoctor.ast.ListItem;
+import org.asciidoctor.ast.StructuralNode;
+
+public class ListImpl extends StructuralNodeImpl implements List {
+
+    public static final String ORDERED_LIST_CONTEXT = "olist";
+    private static final char ORDERED_LIST_MARKER = '.';
+    public static final String UNORDERED_LIST_CONTEXT = "ulist";
+    private static final char UNORDERED_LIST_MARKER = '*';
+
+    public ListImpl(final ContentNode parent) {
+        super(parent);
+        setContext(ORDERED_LIST_CONTEXT);
+    }
+
+    @Override
+    public void formatTo(final StringBuilder buffer) {
+        final String prefix = computeItemPrefix();
+        getBlocks().forEach(node -> {
+            buffer.append(prefix);
+            if (node instanceof final StringBuilderFormattable formattable) {
+                formattable.formatTo(buffer);
+            } else {
+                buffer.append(node.convert());
+            }
+        });
+    }
+
+    /**
+     * Computes the appropriate prefix for the list.
+     */
+    private String computeItemPrefix() {
+        final StringBuilder sb = new StringBuilder();
+        ContentNode currentNode = this;
+        while (currentNode instanceof List) {
+            // If the type of list changes stop.
+            if (!Objects.equals(getContext(), currentNode.getContext())) {
+                break;
+            }
+            sb.append(
+                    ORDERED_LIST_CONTEXT.equals(currentNode.getContext())
+                            ? ORDERED_LIST_MARKER
+                            : UNORDERED_LIST_MARKER);
+            currentNode = currentNode.getParent();
+            if (!(currentNode instanceof ListItem)) {
+                break;
+            }
+            currentNode = currentNode.getParent();
+        }
+        return sb.reverse().append(' ').toString();
+    }
+
+    @Override
+    public java.util.List<StructuralNode> getItems() {
+        return getBlocks();
+    }
+
+    @Override
+    public boolean hasItems() {
+        return !getBlocks().isEmpty();
+    }
+
+    @Override
+    public void setContext(final String context) {
+        switch (context) {
+            case ORDERED_LIST_CONTEXT:
+            case UNORDERED_LIST_CONTEXT:
+                break;
+            default:
+                throw new RuntimeException("Unknown list context " + context);
+        }
+        super.setContext(context);
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ListItemImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ListItemImpl.java
new file mode 100644
index 0000000..2988041
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ListItemImpl.java
@@ -0,0 +1,74 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.asciidoctor.ast.ContentNode;
+import org.asciidoctor.ast.ListItem;
+import org.asciidoctor.ast.StructuralNode;
+
+public class ListItemImpl extends StructuralNodeImpl implements ListItem {
+
+    public ListItemImpl(final ContentNode parent) {
+        super(parent);
+    }
+
+    @Override
+    public void formatTo(final StringBuilder buffer) {
+        boolean first = true;
+        for (final StructuralNode node : getBlocks()) {
+            if (!first) {
+                buffer.append("+\n");
+            } else {
+                first = false;
+            }
+            if (node instanceof final StringBuilderFormattable formattable) {
+                formattable.formatTo(buffer);
+            } else {
+                buffer.append(node.convert());
+            }
+        }
+    }
+
+    //
+    // All methods below this line are not implemented.
+    //
+    @Override
+    public String getMarker() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getText() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getSource() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setSource(final String source) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasText() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/RowImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/RowImpl.java
new file mode 100644
index 0000000..6dff898
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/RowImpl.java
@@ -0,0 +1,32 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.asciidoctor.ast.Cell;
+import org.asciidoctor.ast.Row;
+
+public class RowImpl implements Row {
+
+    private final List<Cell> cells = new ArrayList<>();
+
+    @Override
+    public List<Cell> getCells() {
+        return cells;
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/SectionImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/SectionImpl.java
new file mode 100644
index 0000000..6824f15
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/SectionImpl.java
@@ -0,0 +1,108 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.asciidoctor.ast.ContentNode;
+import org.asciidoctor.ast.Document;
+import org.asciidoctor.ast.Section;
+import org.asciidoctor.ast.StructuralNode;
+
+public class SectionImpl extends StructuralNodeImpl implements Section {
+
+    private final List<StructuralNode> content = new ArrayList<>();
+
+    public SectionImpl(final ContentNode parent) {
+        super(parent);
+    }
+
+    @Override
+    public void formatTo(final StringBuilder buffer) {
+        final String title = getTitle();
+        if (title != null) {
+            buffer.append("=".repeat(computeSectionLevel(this)))
+                    .append(' ')
+                    .append(title)
+                    .append("\n\n");
+        }
+        boolean first = true;
+        for (final StructuralNode node : getBlocks()) {
+            if (!first) {
+                buffer.append('\n');
+            } else {
+                first = false;
+            }
+            if (node instanceof final StringBuilderFormattable formattable) {
+                formattable.formatTo(buffer);
+            } else {
+                buffer.append(node.convert());
+            }
+        }
+    }
+
+    private static int computeSectionLevel(final StructuralNode section) {
+        int level = 0;
+        StructuralNode currentNode = section;
+        while (currentNode != null) {
+            if (currentNode instanceof Section || currentNode instanceof 
Document) {
+                level++;
+            }
+            currentNode = (StructuralNode) currentNode.getParent();
+        }
+        return level;
+    }
+
+    @Override
+    public String getSectionName() {
+        return getTitle();
+    }
+
+    //
+    // All methods below this line are not implemented.
+    //
+    @Override
+    public int getIndex() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getNumeral() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isSpecial() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isNumbered() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getSectnum() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getSectnum(final String delimiter) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/StructuralNodeImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/StructuralNodeImpl.java
new file mode 100644
index 0000000..402ae54
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/StructuralNodeImpl.java
@@ -0,0 +1,158 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.asciidoctor.ast.ContentNode;
+import org.asciidoctor.ast.Cursor;
+import org.asciidoctor.ast.StructuralNode;
+
+public abstract class StructuralNodeImpl extends ContentNodeImpl implements 
StructuralNode, StringBuilderFormattable {
+
+    private int level;
+    private String title;
+    private String style;
+
+    private final List<StructuralNode> blocks = new ArrayList<>();
+
+    public StructuralNodeImpl(final ContentNode parent) {
+        super(parent);
+    }
+
+    @Override
+    public String getTitle() {
+        return title;
+    }
+
+    @Override
+    public void setTitle(final String title) {
+        this.title = title;
+    }
+
+    @Override
+    public String getStyle() {
+        return style;
+    }
+
+    @Override
+    public void setStyle(final String style) {
+        this.style = style;
+    }
+
+    @Override
+    public List<StructuralNode> getBlocks() {
+        return blocks;
+    }
+
+    @Override
+    public void append(final StructuralNode structuralNode) {
+        blocks.add(structuralNode);
+    }
+
+    @Override
+    public String convert() {
+        final StringBuilder sb = new StringBuilder();
+        formatTo(sb);
+        return sb.toString();
+    }
+
+    @Override
+    public int getLevel() {
+        return level;
+    }
+
+    @Override
+    public void setLevel(final int level) {
+        this.level = level;
+    }
+
+    //
+    // All methods below this line are not implemented.
+    //
+    @Override
+    public List<StructuralNode> findBy(final Map<Object, Object> map) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getCaption() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setCaption(final String caption) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getContentModel() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Cursor getSourceLocation() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<String> getSubstitutions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isSubstitutionEnabled(final String substitutionEnabled) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeSubstitution(final String substitution) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void addSubstitution(final String substitution) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void prependSubstitution(final String substitution) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setSubstitutions(final String... substitutions) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getContent() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isInline() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isBlock() {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/TableImpl.java
 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/TableImpl.java
new file mode 100644
index 0000000..c57b179
--- /dev/null
+++ 
b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/TableImpl.java
@@ -0,0 +1,120 @@
+/*
+ * 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.logging.log4j.docgen.processor.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.asciidoctor.ast.Column;
+import org.asciidoctor.ast.ContentNode;
+import org.asciidoctor.ast.Row;
+import org.asciidoctor.ast.Table;
+
+public class TableImpl extends StructuralNodeImpl implements Table {
+
+    private final List<Row> header = new ArrayList<>();
+    private final List<Row> body = new ArrayList<>();
+
+    public TableImpl(final ContentNode parent) {
+        super(parent);
+    }
+
+    @Override
+    public void formatTo(final StringBuilder buffer) {
+        final int colCount = header.isEmpty()
+                ? body.isEmpty() ? 1 : body.get(0).getCells().size()
+                : header.get(0).getCells().size();
+
+        buffer.append("[cols=\"");
+        formatColSpecifier(colCount, buffer);
+        if (!header.isEmpty()) {
+            buffer.append("\",options=\"headers");
+        }
+        buffer.append("\"]\n").append("|===\n");
+        getHeader().forEach(row -> formatRow(row, buffer));
+        getBody().forEach(row -> formatRow(row, buffer));
+        buffer.append("\n|===\n");
+    }
+
+    private static void formatColSpecifier(final int colCount, final 
StringBuilder buffer) {
+        if (colCount > 0) {
+            buffer.append("1");
+        }
+        for (int i = 1; i < colCount; i++) {
+            buffer.append(",1");
+        }
+    }
+
+    private void formatRow(final Row row, final StringBuilder buffer) {
+        buffer.append('\n');
+        row.getCells().forEach(cell -> {
+            if (cell instanceof final StringBuilderFormattable formattable) {
+                formattable.formatTo(buffer);
+            } else {
+                buffer.append(cell.convert());
+            }
+        });
+    }
+
+    @Override
+    public List<Row> getHeader() {
+        return header;
+    }
+
+    @Override
+    public List<Row> getBody() {
+        return body;
+    }
+
+    //
+    // All methods below this line are not implemented.
+    //
+    @Override
+    public List<Row> getFooter() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasHeaderOption() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<Column> getColumns() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getFrame() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setFrame(final String frame) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getGrid() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setGrid(final String grid) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/log4j-docgen/src/test/it/example/JavadocExample.java 
b/log4j-docgen/src/test/it/example/JavadocExample.java
new file mode 100644
index 0000000..5ddf2c4
--- /dev/null
+++ b/log4j-docgen/src/test/it/example/JavadocExample.java
@@ -0,0 +1,100 @@
+/*
+ * 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 example;
+
+/**
+ * Example of JavaDoc to AsciiDoc conversion
+ * <p>
+ *     We run the {@code javadoc} tool on this class to test conversion of 
JavaDoc comments to AsciiDoc. This
+ *     paragraph has two sentences.
+ * </p>
+ * <p>
+ *     Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum 
blandit dictum sem, ornare posuere lorem
+ *     convallis sit amet. Sed dui augue, faucibus ut nisi id, mollis euismod 
nibh. Donec lobortis luctus viverra. In
+ *     orci ante, pretium et fringilla at, sagittis nec justo. Cras finibus 
lorem vel volutpat interdum. Sed laoreet
+ *     libero eros, ac cursus nibh dapibus vitae. Integer ante lorem, rhoncus 
at tortor vel, congue accumsan lorem.
+ *     In hac habitasse platea dictumst. Nunc luctus ornare justo. Etiam ut 
metus id tortor dignissim semper. Nam
+ *     turpis dui, aliquet nec enim et, faucibus accumsan dui.
+ *</p>
+ * <p>
+ *     Aenean tincidunt elit id posuere mattis. Fusce bibendum sapien sed 
risus ultricies, non molestie erat volutpat.
+ *     Donec nisi felis, egestas eu lobortis id, vulputate nec felis. In at 
dui euismod, blandit nulla et, accumsan
+ *     elit. Proin id venenatis dui. Suspendisse sit amet est ut neque 
tincidunt venenatis. Donec bibendum quis velit
+ *     fermentum porttitor. Maecenas faucibus, eros sit amet maximus 
malesuada, turpis neque bibendum justo, eu
+ *     vehicula justo metus a ipsum. In at ullamcorper ipsum. Quisque in 
vehicula erat. Proin vitae suscipit dui,
+ *     rutrum hendrerit augue. Curabitur finibus feugiat elit.
+ * </p>
+ * <ol>
+ *     <li>Item with a nested ordered list.
+ *         <ol>
+ *             <li>First nested item.</li>
+ *             <li>Second nested item.</li>
+ *         </ol>
+ *     </li>
+ *     <li>Item with a nested unordered list.
+ *         <ul>
+ *             <li>Unordered list item.</li>
+ *         </ul>
+ *     </li>
+ *     <li>
+ *         <p>
+ *             Item with complex content
+ *         </p>
+ *         <p>
+ *             Mauris suscipit velit nec ligula mattis, nec varius augue 
accumsan. Curabitur a dolor dui. Quisque
+ *             congue facilisis est nec dignissim. Pellentesque egestas 
eleifend faucibus. Fusce imperdiet ligula a
+ *             lectus fringilla varius. Sed malesuada porta vulputate. Sed 
vulputate purus nec nibh interdum
+ *             convallis. Cras faucibus, dolor tempus lacinia vehicula, elit 
risus luctus libero, sed molestie nisi
+ *             lorem sit amet enim. Integer vitae enim sagittis, malesuada 
lorem at, interdum tellus. Suspendisse
+ *             potenti. Vestibulum ac nisi sit amet ex dictum suscipit. Nulla 
varius augue a velit tincidunt feugiat.
+ *             Proin fringilla id leo ut dignissim. Vivamus eu tellus eget 
orci suscipit viverra. Donec sodales et
+ *             arcu vel mollis.
+ *         </p>
+ *         <p>
+ *             Praesent gravida auctor lectus quis interdum. Etiam semper 
mauris quis neque bibendum molestie.
+ *             Maecenas a lacus nec risus pellentesque accumsan. Suspendisse 
dictum dui eleifend nibh facilisis, non
+ *             consequat neque elementum. Donec scelerisque ultricies ipsum, 
pretium elementum ex pellentesque
+ *             malesuada. Mauris egestas massa vitae sapien lobortis 
convallis. Donec feugiat, purus commodo
+ *             consequat vehicula, dolor urna aliquam arcu, id rutrum quam 
tortor quis libero. Sed varius justo eget
+ *             congue lacinia.
+ *         </p>
+ *     </li>
+ * </ol>
+ * <ul>
+ *     <li>Item of an unordered list.</li>
+ * </ul>
+ * <h2>First section</h2>
+ * <table>
+ *     <tr>
+ *         <th>Key</th>
+ *         <th>Value</th>
+ *     </tr>
+ *     <tr>
+ *         <td>A</td>
+ *         <td>1</td>
+ *     </tr>
+ *     <tr>
+ *         <td>B</td>
+ *         <td>2</td>
+ *     </tr>
+ * </table>
+ * <h3>Subsection</h3>
+ * <pre>
+ *     private static final Logger logger = LogManager.getLogger();
+ * </pre>
+ */
+public class JavadocExample {}
diff --git 
a/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/processor/AsciidocConverterTest.java
 
b/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/processor/AsciidocConverterTest.java
new file mode 100644
index 0000000..5eb71b7
--- /dev/null
+++ 
b/log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/processor/AsciidocConverterTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.logging.log4j.docgen.processor;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.DocumentationTool;
+import javax.tools.DocumentationTool.DocumentationTask;
+import javax.tools.FileObject;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+import jdk.javadoc.doclet.Doclet;
+import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class AsciidocConverterTest {
+
+    private static final Path LICENSE_PATH;
+
+    static {
+        try {
+            LICENSE_PATH = Paths.get(AsciidocConverterTest.class
+                    .getResource("/templates/license.ftl")
+                    .toURI());
+        } catch (final URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    void convertToAsciidoc() throws Exception {
+        final DocumentationTool tool = 
ToolProvider.getSystemDocumentationTool();
+        final DiagnosticCollector<JavaFileObject> ds = new 
DiagnosticCollector<>();
+        final StandardJavaFileManager fileManager = 
tool.getStandardFileManager(null, Locale.ROOT, UTF_8);
+
+        final Path basePath = Paths.get(System.getProperty("basedir", "."));
+        final Path sourcePath = 
basePath.resolve("src/test/it/example/JavadocExample.java");
+        final Iterable<? extends JavaFileObject> sources = 
fileManager.getJavaFileObjectsFromPaths(List.of(sourcePath));
+
+        final Path destPath = basePath.resolve("target/test-site");
+        Files.createDirectories(destPath);
+        fileManager.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, 
Set.of(destPath));
+
+        final DocumentationTask task = tool.getTask(null, fileManager, ds, 
TestDoclet.class, null, sources);
+        task.call();
+
+        final List<String> warnings = ds.getDiagnostics().stream()
+                .filter(d -> d.getKind() != Diagnostic.Kind.NOTE)
+                .map(d -> d.getMessage(Locale.ROOT))
+                .toList();
+        Assertions.assertThat(warnings).isEmpty();
+        final Path expectedPath = Paths.get(AsciidocConverterTest.class
+                .getResource("/expected/processor/JavadocExample.adoc")
+                .toURI());
+        
Assertions.assertThat(destPath.resolve("processor/JavadocExample.adoc"))
+                .hasSameTextualContentAs(expectedPath, UTF_8);
+    }
+
+    public static class TestDoclet implements Doclet {
+
+        @Override
+        public void init(final Locale locale, final Reporter reporter) {}
+
+        @Override
+        public String getName() {
+            return "test";
+        }
+
+        @Override
+        public Set<? extends Option> getSupportedOptions() {
+            return Set.of();
+        }
+
+        @Override
+        public SourceVersion getSupportedSourceVersion() {
+            return SourceVersion.RELEASE_17;
+        }
+
+        @Override
+        public boolean run(final DocletEnvironment environment) {
+            final AsciidocConverter converter = new 
AsciidocConverter(environment.getDocTrees());
+            final JavaFileManager fileManager = 
environment.getJavaFileManager();
+            try {
+                for (final Element element : 
environment.getIncludedElements()) {
+                    if 
("JavadocExample".equals(element.getSimpleName().toString())) {
+                        final FileObject output = fileManager.getFileForOutput(
+                                StandardLocation.CLASS_OUTPUT, "processor", 
"JavadocExample.adoc", null);
+                        final String asciiDoc = converter.toAsciiDoc(element);
+                        try (final OutputStream os = 
output.openOutputStream()) {
+                            Files.copy(LICENSE_PATH, os);
+                            os.write(asciiDoc.getBytes(UTF_8));
+                        }
+                    }
+                }
+            } catch (final IOException e) {
+                throw new RuntimeException(e);
+            }
+            return true;
+        }
+    }
+}
diff --git 
a/log4j-docgen/src/test/resources/expected/processor/JavadocExample.adoc 
b/log4j-docgen/src/test/resources/expected/processor/JavadocExample.adoc
new file mode 100644
index 0000000..b14e4e6
--- /dev/null
+++ b/log4j-docgen/src/test/resources/expected/processor/JavadocExample.adoc
@@ -0,0 +1,107 @@
+////
+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
+
+    https://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.
+////
+= JavadocExample
+
+Example of JavaDoc to AsciiDoc conversion
+
+We run the `javadoc` tool on this class to test conversion of JavaDoc comments 
to AsciiDoc.
+This paragraph has two sentences.
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Vestibulum blandit dictum sem, ornare posuere lorem convallis sit amet.
+Sed dui augue, faucibus ut nisi id, mollis euismod nibh.
+Donec lobortis luctus viverra.
+In orci ante, pretium et fringilla at, sagittis nec justo.
+Cras finibus lorem vel volutpat interdum.
+Sed laoreet libero eros, ac cursus nibh dapibus vitae.
+Integer ante lorem, rhoncus at tortor vel, congue accumsan lorem.
+In hac habitasse platea dictumst.
+Nunc luctus ornare justo.
+Etiam ut metus id tortor dignissim semper.
+Nam turpis dui, aliquet nec enim et, faucibus accumsan dui.
+
+Aenean tincidunt elit id posuere mattis.
+Fusce bibendum sapien sed risus ultricies, non molestie erat volutpat.
+Donec nisi felis, egestas eu lobortis id, vulputate nec felis.
+In at dui euismod, blandit nulla et, accumsan elit.
+Proin id venenatis dui.
+Suspendisse sit amet est ut neque tincidunt venenatis.
+Donec bibendum quis velit fermentum porttitor.
+Maecenas faucibus, eros sit amet maximus malesuada, turpis neque bibendum 
justo, eu vehicula justo metus a ipsum.
+In at ullamcorper ipsum.
+Quisque in vehicula erat.
+Proin vitae suscipit dui, rutrum hendrerit augue.
+Curabitur finibus feugiat elit.
+
+. Item with a nested ordered list.
++
+.. First nested item.
+.. Second nested item.
+. Item with a nested unordered list.
++
+* Unordered list item.
+. Item with complex content
++
+Mauris suscipit velit nec ligula mattis, nec varius augue accumsan.
+Curabitur a dolor dui.
+Quisque congue facilisis est nec dignissim.
+Pellentesque egestas eleifend faucibus.
+Fusce imperdiet ligula a lectus fringilla varius.
+Sed malesuada porta vulputate.
+Sed vulputate purus nec nibh interdum convallis.
+Cras faucibus, dolor tempus lacinia vehicula, elit risus luctus libero, sed 
molestie nisi lorem sit amet enim.
+Integer vitae enim sagittis, malesuada lorem at, interdum tellus.
+Suspendisse potenti.
+Vestibulum ac nisi sit amet ex dictum suscipit.
+Nulla varius augue a velit tincidunt feugiat.
+Proin fringilla id leo ut dignissim.
+Vivamus eu tellus eget orci suscipit viverra.
+Donec sodales et arcu vel mollis.
++
+Praesent gravida auctor lectus quis interdum.
+Etiam semper mauris quis neque bibendum molestie.
+Maecenas a lacus nec risus pellentesque accumsan.
+Suspendisse dictum dui eleifend nibh facilisis, non consequat neque elementum.
+Donec scelerisque ultricies ipsum, pretium elementum ex pellentesque malesuada.
+Mauris egestas massa vitae sapien lobortis convallis.
+Donec feugiat, purus commodo consequat vehicula, dolor urna aliquam arcu, id 
rutrum quam tortor quis libero.
+Sed varius justo eget congue lacinia.
+
+* Item of an unordered list.
+
+== First section
+
+[cols="1,1",options="headers"]
+|===
+
+| Key
+| Value
+
+| A
+| 1
+
+| B
+| 2
+
+|===
+
+=== Subsection
+
+[source]
+----
+     private static final Logger logger = LogManager.getLogger();
+----
diff --git a/log4j-tools-parent/pom.xml b/log4j-tools-parent/pom.xml
index 73d8708..02871c4 100644
--- a/log4j-tools-parent/pom.xml
+++ b/log4j-tools-parent/pom.xml
@@ -34,9 +34,11 @@
     <!-- Dependency versions -->
     <assertj.version>3.25.1</assertj.version>
     <commons-io.version>2.15.1</commons-io.version>
+    <commons-lang3.version>3.14.0</commons-lang3.version>
     <freemarker.version>2.3.32</freemarker.version>
     <jakarta.inject.version>1.0.5</jakarta.inject.version>
     <junit.version>5.10.1</junit.version>
+    <log4j-bom.version>3.0.0-beta1</log4j-bom.version>
     <modello.version>2.1.2</modello.version>
     <xmlunit.version>2.9.1</xmlunit.version>
 
@@ -51,6 +53,14 @@
   <dependencyManagement>
     <dependencies>
 
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-bom</artifactId>
+        <version>${log4j-bom.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+
       <dependency>
         <groupId>org.junit</groupId>
         <artifactId>junit-bom</artifactId>
@@ -71,6 +81,12 @@
         <version>${commons-io.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-lang3</artifactId>
+        <version>${commons-lang3.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.freemarker</groupId>
         <artifactId>freemarker</artifactId>

Reply via email to