Skip to content

Commit

Permalink
Add Javadoc to Asciidoc converter
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ppkarwasz committed Feb 2, 2024
1 parent 5eb5e66 commit 40e29fc
Show file tree
Hide file tree
Showing 18 changed files with 1,936 additions and 0 deletions.
28 changes: 28 additions & 0 deletions log4j-docgen/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Void, AsciidocData> {

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 <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 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 <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();
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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Void, AsciidocData> DOC_COMMENT_TREE_VISITOR = new DocCommentTreeVisitor();
private static final DocTreeVisitor<Void, AsciidocData> 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);
}
}
}
Loading

0 comments on commit 40e29fc

Please sign in to comment.