From 06ca02ac972a84f5addc3447b59668f0fde12555 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Wed, 31 Jan 2024 23:18:27 +0100
Subject: [PATCH 1/6] 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 | 31 +++
.../AbstractAsciidocTreeVisitor.java | 233 ++++++++++++++++++
.../docgen/processor/AsciidocConverter.java | 86 +++++++
.../log4j/docgen/processor/AsciidocData.java | 149 +++++++++++
.../docgen/processor/internal/BlockImpl.java | 73 ++++++
.../docgen/processor/internal/CellImpl.java | 113 +++++++++
.../processor/internal/ContentNodeImpl.java | 190 ++++++++++++++
.../processor/internal/DocumentImpl.java | 123 +++++++++
.../docgen/processor/internal/ListImpl.java | 96 ++++++++
.../processor/internal/ListItemImpl.java | 74 ++++++
.../docgen/processor/internal/RowImpl.java | 32 +++
.../processor/internal/SectionImpl.java | 108 ++++++++
.../internal/StructuralNodeImpl.java | 158 ++++++++++++
.../docgen/processor/internal/TableImpl.java | 120 +++++++++
.../src/test/it/example/JavadocExample.java | 100 ++++++++
.../processor/AsciidocConverterTest.java | 132 ++++++++++
.../expected/processor/JavadocExample.adoc | 105 ++++++++
log4j-tools-parent/pom.xml | 16 ++
18 files changed, 1939 insertions(+)
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AbstractAsciidocTreeVisitor.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocConverter.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocData.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/BlockImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/CellImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ContentNodeImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/DocumentImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ListImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/ListItemImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/RowImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/SectionImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/StructuralNodeImpl.java
create mode 100644 log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/internal/TableImpl.java
create mode 100644 log4j-docgen/src/test/it/example/JavadocExample.java
create mode 100644 log4j-docgen/src/test/java/org/apache/logging/log4j/docgen/processor/AsciidocConverterTest.java
create mode 100644 log4j-docgen/src/test/resources/expected/processor/JavadocExample.adoc
diff --git a/log4j-docgen/pom.xml b/log4j-docgen/pom.xml
index d8d735f4..a3b5e606 100644
--- a/log4j-docgen/pom.xml
+++ b/log4j-docgen/pom.xml
@@ -27,7 +27,11 @@
log4j-docgen
+ 17false
+
+
+ 3.0.0-alpha.2
@@ -38,11 +42,38 @@
provided
+
+ org.asciidoctor
+ asciidoctorj-api
+ ${asciidoctorj-api.version}
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
org.freemarkerfreemarker
+
+ org.apache.logging.log4j
+ log4j-api
+
+
+
+ org.apache.logging.log4j
+ log4j-plugins
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ test
+
+
org.junit.jupiterjunit-jupiter-api
diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AbstractAsciidocTreeVisitor.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AbstractAsciidocTreeVisitor.java
new file mode 100644
index 00000000..00b4fbe8
--- /dev/null
+++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AbstractAsciidocTreeVisitor.java
@@ -0,0 +1,233 @@
+/*
+ * 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 org.apache.commons.lang3.StringUtils.substringBefore;
+
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.EndElementTree;
+import com.sun.source.doctree.LinkTree;
+import com.sun.source.doctree.LiteralTree;
+import com.sun.source.doctree.StartElementTree;
+import com.sun.source.doctree.TextTree;
+import com.sun.source.util.SimpleDocTreeVisitor;
+import java.util.ArrayList;
+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.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.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;
+
+class AbstractAsciidocTreeVisitor extends SimpleDocTreeVisitor {
+
+ private static void appendSentences(final String text, final AsciidocData data) {
+ final String body = StringUtils.normalizeSpace(text);
+ final String[] sentences = body.split("(?<=\\w{2}[.!?])", -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].strip());
+ }
+
+ @Override
+ public Void visitStartElement(final StartElementTree node, final AsciidocData 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
tag must be a child of a or
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 AsciidocData 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
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 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();
+ if (cell instanceof Cell) {
+ 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 visitLink(final LinkTree node, final AsciidocData data) {
+ final String className = substringBefore(node.getReference().getSignature(), '#');
+ final String simpleName = StringUtils.substringAfterLast(className, '.');
+ if (!data.getCurrentLine().isEmpty()) {
+ data.append(" ");
+ }
+ data.append("xref:")
+ .append(className)
+ .append(".adoc[")
+ .append(simpleName)
+ .append("]");
+ return super.visitLink(node, data);
+ }
+
+ @Override
+ public Void visitLiteral(final LiteralTree node, final AsciidocData 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 AsciidocData 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);
+ }
+}
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 00000000..19fe0e1a
--- /dev/null
+++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocConverter.java
@@ -0,0 +1,86 @@
+/*
+ * 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.ParamTree;
+import com.sun.source.util.DocTrees;
+import javax.lang.model.element.Element;
+
+/**
+ * Converts a {@link DocCommentTree} into AsciiDoc text.
+ */
+class AsciidocConverter {
+
+ private static final DocTreeVisitor DOC_COMMENT_TREE_VISITOR = new DocCommentTreeVisitor();
+ private static final DocTreeVisitor PARAM_TREE_VISITOR = new ParamTreeVisitor();
+
+ 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) : null;
+ }
+
+ public String toAsciiDoc(final DocCommentTree tree) {
+ final AsciidocData data = new AsciidocData();
+ tree.accept(DOC_COMMENT_TREE_VISITOR, data);
+ return data.getDocument().convert();
+ }
+
+ public String toAsciiDoc(final ParamTree tree) {
+ final AsciidocData data = new AsciidocData();
+ tree.accept(PARAM_TREE_VISITOR, data);
+ return data.getDocument().convert();
+ }
+
+ private static class DocCommentTreeVisitor extends AbstractAsciidocTreeVisitor {
+ @Override
+ public Void visitDocComment(final DocCommentTree node, final AsciidocData 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);
+ }
+ }
+
+ private static class ParamTreeVisitor extends AbstractAsciidocTreeVisitor {
+ @Override
+ public Void visitParam(final ParamTree node, final AsciidocData data) {
+ for (final DocTree docTree : node.getDescription()) {
+ docTree.accept(this, data);
+ }
+ // Flushes the last paragraph
+ data.newParagraph();
+ return super.visitParam(node, data);
+ }
+ }
+}
diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocData.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocData.java
new file mode 100644
index 00000000..a35e8293
--- /dev/null
+++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocData.java
@@ -0,0 +1,149 @@
+/*
+ * 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 java.util.EmptyStackException;
+import java.util.function.Function;
+import org.apache.logging.log4j.docgen.processor.internal.BlockImpl;
+import org.apache.logging.log4j.docgen.processor.internal.DocumentImpl;
+import org.apache.logging.log4j.docgen.processor.internal.SectionImpl;
+import org.asciidoctor.ast.Block;
+import org.asciidoctor.ast.Document;
+import org.asciidoctor.ast.StructuralNode;
+
+class AsciidocData {
+ private final Document document;
+ private int currentSectionLevel;
+ private StructuralNode currentNode;
+ // not attached to the current node
+ private Block currentParagraph;
+ private final StringBuilder currentLine;
+
+ public AsciidocData() {
+ document = new DocumentImpl();
+ 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 AsciidocData 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) {
+ if (words.isBlank()) {
+ return;
+ }
+ // Separate text from previous words
+ if (!currentLine.isEmpty() && Character.isAlphabetic(words.codePointAt(0))) {
+ currentLine.append(" ");
+ }
+ currentLine.append(words);
+ }
+
+ public void newParagraph() {
+ newLine();
+ final java.util.List 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 void 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;
+ }
+}
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 00000000..a2dbfd8d
--- /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 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 getLines() {
+ return lines;
+ }
+
+ @Override
+ public void setLines(final List 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 00000000..a28c9a90
--- /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 00000000..63e2b915
--- /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 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 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
*
+ * A sentence with foo, foo`, foobar. Another sentence with {@code foo},
+ * {@code foo`}, {@code foo}bar.
+ *
+ *
+ * We can use strongemphasis too, or we can use bold and italic.
+ *
+ *
* 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
diff --git a/log4j-docgen/src/test/resources/expected/processor/JavadocExample.adoc b/log4j-docgen/src/test/resources/expected/processor/JavadocExample.adoc
index 1afe6570..9986f396 100644
--- a/log4j-docgen/src/test/resources/expected/processor/JavadocExample.adoc
+++ b/log4j-docgen/src/test/resources/expected/processor/JavadocExample.adoc
@@ -19,6 +19,11 @@ 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.
+A sentence with `foo`, `++foo`++`, `foo` bar.
+Another sentence with `foo`, `++foo`++`, `foo` bar.
+
+We can use *strong* _emphasis_ too, or we can use *bold* and _italic_.
+
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.
From 51994c80f3c10e11da98868ca25e1e41248f1da3 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Thu, 8 Feb 2024 11:47:50 +0100
Subject: [PATCH 4/6] Add support for simple code language detection and
formatting
If we encounter a `
` element with content containing XML elements,
we classify it as XML, otherwise Java.
The maximal common indentation is stripped from the content of a `
`
tag.
---
.../AbstractAsciidocTreeVisitor.java | 81 ++++++++++++++-----
.../docgen/processor/AsciidocConverter.java | 19 +++--
.../log4j/docgen/processor/AsciidocData.java | 27 +++++--
.../src/test/it/example/JavadocExample.java | 23 +++++-
.../expected/processor/JavadocExample.adoc | 32 +++++++-
5 files changed, 144 insertions(+), 38 deletions(-)
diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AbstractAsciidocTreeVisitor.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AbstractAsciidocTreeVisitor.java
index 609cca7d..1901579d 100644
--- a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AbstractAsciidocTreeVisitor.java
+++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AbstractAsciidocTreeVisitor.java
@@ -20,12 +20,15 @@
import com.sun.source.doctree.DocTree;
import com.sun.source.doctree.EndElementTree;
+import com.sun.source.doctree.EntityTree;
import com.sun.source.doctree.LinkTree;
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.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.docgen.processor.internal.BlockImpl;
import org.apache.logging.log4j.docgen.processor.internal.CellImpl;
@@ -45,7 +48,8 @@
abstract class AbstractAsciidocTreeVisitor extends SimpleDocTreeVisitor {
- private static final String SOURCE_STYLE = "source";
+ private static final String JAVA_SOURCE_STYLE = "source,java";
+ private static final String XML_SOURCE_STYLE = "source,xml";
// These are not supported by AsciiDoctor and are only used internally
private static final String CODE_STYLE = "code";
private static final String CODE_DELIM = "`";
@@ -54,14 +58,17 @@ abstract class AbstractAsciidocTreeVisitor extends SimpleDocTreeVisitor");
+
+ /**
+ * Used to convert entities into strings.
+ */
+ private final DocTrees docTrees;
+
+ AbstractAsciidocTreeVisitor(final DocTrees docTrees) {
+ this.docTrees = docTrees;
}
@Override
@@ -120,7 +127,6 @@ public Void visitStartElement(final StartElementTree node, final AsciidocData da
case "pre":
data.newParagraph();
data.getCurrentParagraph().setContext(BlockImpl.LISTING_CONTEXT);
- data.getCurrentParagraph().setStyle(SOURCE_STYLE);
break;
case "code":
data.newTextSpan(CODE_STYLE);
@@ -175,7 +181,23 @@ public Void visitEndElement(final EndElementTree node, final AsciidocData data)
data.getCurrentNode().setTitle(title);
break;
case "pre":
- data.newParagraph();
+ final Block codeBlock = data.newParagraph();
+ final java.util.List lines = codeBlock.getLines();
+ // Trim common indentation and detect language
+ int commonIndentSize = Integer.MAX_VALUE;
+ for (final String line : lines) {
+ final int firstNotSpace = StringUtils.indexOfAnyBut(line, SPACE);
+ if (0 <= firstNotSpace && firstNotSpace < commonIndentSize) {
+ commonIndentSize = firstNotSpace;
+ }
+ }
+ final boolean isXml = XML_TAG.matcher(String.join(SPACE, lines)).find();
+ final java.util.List newLines = new ArrayList<>(lines.size());
+ for (final String line : lines) {
+ newLines.add(StringUtils.substring(line, commonIndentSize));
+ }
+ codeBlock.setLines(newLines);
+ codeBlock.setStyle(isXml ? XML_SOURCE_STYLE : JAVA_SOURCE_STYLE);
break;
case "tr":
// We group the new cells into a row
@@ -247,16 +269,13 @@ public Void visitLiteral(final LiteralTree node, final AsciidocData data) {
return super.visitLiteral(node, data);
}
- private void appendSpan(final AsciidocData data, final String delimiter) {
- final String body = data.popTextSpan();
- data.append(delimiter);
- final boolean needsEscaping = body.contains(delimiter);
- if (needsEscaping) {
- data.append("++").append(body).append("++");
- } else {
- data.append(body);
+ @Override
+ public Void visitEntity(final EntityTree node, final AsciidocData asciidocData) {
+ final String text = docTrees.getCharacters(node);
+ if (text != null) {
+ asciidocData.append(text);
}
- data.append(delimiter);
+ return super.visitEntity(node, asciidocData);
}
@Override
@@ -269,4 +288,26 @@ public Void visitText(final TextTree node, final AsciidocData data) {
}
return super.visitText(node, data);
}
+
+ private static void appendSentences(final String text, final AsciidocData data) {
+ final String[] sentences = text.split("(?<=\\w{2}[.!?])", -1);
+ // Full sentences
+ for (int i = 0; i < sentences.length - 1; i++) {
+ data.appendAdjustingSpace(sentences[i]).newLine();
+ }
+ // Partial sentence
+ data.appendAdjustingSpace(sentences[sentences.length - 1]);
+ }
+
+ private void appendSpan(final AsciidocData data, final String delimiter) {
+ final String body = data.popTextSpan();
+ data.append(delimiter);
+ final boolean needsEscaping = body.contains(delimiter);
+ if (needsEscaping) {
+ data.append("++").append(body).append("++");
+ } else {
+ data.append(body);
+ }
+ data.append(delimiter);
+ }
}
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
index 93224c2b..2fa76180 100644
--- 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
@@ -28,13 +28,14 @@
*/
final class AsciidocConverter {
- private static final DocTreeVisitor DOC_COMMENT_TREE_VISITOR = new DocCommentTreeVisitor();
- private static final DocTreeVisitor PARAM_TREE_VISITOR = new ParamTreeVisitor();
-
private final DocTrees docTrees;
+ private final DocTreeVisitor docCommentTreeVisitor;
+ private final DocTreeVisitor paramTreeVisitor;
AsciidocConverter(final DocTrees docTrees) {
this.docTrees = docTrees;
+ this.docCommentTreeVisitor = new DocCommentTreeVisitor(docTrees);
+ this.paramTreeVisitor = new ParamTreeVisitor(docTrees);
}
public String toAsciiDoc(final Element element) {
@@ -44,17 +45,21 @@ public String toAsciiDoc(final Element element) {
public String toAsciiDoc(final DocCommentTree tree) {
final AsciidocData data = new AsciidocData();
- tree.accept(DOC_COMMENT_TREE_VISITOR, data);
+ tree.accept(docCommentTreeVisitor, data);
return data.getDocument().convert();
}
public String toAsciiDoc(final ParamTree tree) {
final AsciidocData data = new AsciidocData();
- tree.accept(PARAM_TREE_VISITOR, data);
+ tree.accept(paramTreeVisitor, data);
return data.getDocument().convert();
}
private static final class DocCommentTreeVisitor extends AbstractAsciidocTreeVisitor {
+ public DocCommentTreeVisitor(final DocTrees docTrees) {
+ super(docTrees);
+ }
+
@Override
public Void visitDocComment(final DocCommentTree node, final AsciidocData data) {
// Summary block wrapped in a new paragraph.
@@ -73,6 +78,10 @@ public Void visitDocComment(final DocCommentTree node, final AsciidocData data)
}
private static final class ParamTreeVisitor extends AbstractAsciidocTreeVisitor {
+ public ParamTreeVisitor(final DocTrees docTrees) {
+ super(docTrees);
+ }
+
@Override
public Void visitParam(final ParamTree node, final AsciidocData data) {
for (final DocTree docTree : node.getDescription()) {
diff --git a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocData.java b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocData.java
index c9c7628f..1208490e 100644
--- a/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocData.java
+++ b/log4j-docgen/src/main/java/org/apache/logging/log4j/docgen/processor/AsciidocData.java
@@ -106,11 +106,11 @@ public String popTextSpan() {
return String.join(SPACE, paragraphs.pop().getLines());
}
- public void newParagraph() {
- newParagraph(currentNode);
+ public Block newParagraph() {
+ return newParagraph(currentNode);
}
- private void newParagraph(final StructuralNode parent) {
+ private Block newParagraph(final StructuralNode parent) {
newLine();
final Block currentParagraph = paragraphs.pop();
final java.util.List lines = currentParagraph.getLines();
@@ -118,12 +118,15 @@ private void newParagraph(final StructuralNode parent) {
for (int i = lines.size() - 1; i >= 0; i--) {
if (lines.get(i).isEmpty()) {
lines.remove(i);
+ } else {
+ break;
}
}
if (!currentParagraph.getLines().isEmpty()) {
currentNode.append(currentParagraph);
}
paragraphs.push(new BlockImpl(parent));
+ return currentParagraph;
}
public StructuralNode getCurrentNode() {
@@ -143,13 +146,21 @@ public Document getDocument() {
}
public void setCurrentSectionLevel(final int sectionLevel) {
- while (sectionLevel < currentSectionLevel) {
+ if (sectionLevel < currentSectionLevel) {
+ // Close all subsections
+ while (sectionLevel < currentSectionLevel) {
+ popNode();
+ currentSectionLevel--;
+ }
+ // Create sibling section
popNode();
- currentSectionLevel--;
- }
- while (sectionLevel > currentSectionLevel) {
pushChildNode(SectionImpl::new);
- currentSectionLevel++;
+ } else {
+ // Create missing section levels
+ while (sectionLevel > currentSectionLevel) {
+ pushChildNode(SectionImpl::new);
+ currentSectionLevel++;
+ }
}
}
diff --git a/log4j-docgen/src/test/it/example/JavadocExample.java b/log4j-docgen/src/test/it/example/JavadocExample.java
index bc76bfd0..80e8fd43 100644
--- a/log4j-docgen/src/test/it/example/JavadocExample.java
+++ b/log4j-docgen/src/test/it/example/JavadocExample.java
@@ -101,7 +101,28 @@
*
*
Subsection
*
- * private static final Logger logger = LogManager.getLogger();
+ * public final class Main {
+ * private static final Logger logger = LogManager.getLogger();
+ *
+ * public static void sayHello() {
+ * logger.info("Hello world!");
+ * }
+ * }
+ *