Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Javadoc to Asciidoc converter #101

Merged
merged 6 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions log4j-docgen/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
<artifactId>log4j-docgen</artifactId>

<properties>
<maven.compiler.release>17</maven.compiler.release>
<bnd.baseline.fail.on.missing>false</bnd.baseline.fail.on.missing>

<!-- Dependency versions -->
<asciidoctorj-api.version>3.0.0-alpha.2</asciidoctorj-api.version>
</properties>

<dependencies>
Expand All @@ -38,11 +42,27 @@
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctorj-api</artifactId>
<version>${asciidoctorj-api.version}</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-plugins</artifactId>
</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,303 @@
/*
* 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.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;
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;

abstract class AbstractAsciidocTreeVisitor extends SimpleDocTreeVisitor<Void, AsciidocData> {

private static final String JAVA_SOURCE_STYLE = "source,java";
private static final String XML_SOURCE_STYLE = "source,xml";

private static final String CODE_DELIM = "`";
private static final String EMPHASIS_DELIM = "_";
private static final String STRONG_EMPHASIS_DELIM = "*";

private static final String SPACE = " ";
// Simple pattern to match (most) XML opening tags
private static final Pattern XML_TAG = Pattern.compile("<\\w+\\s*(\\w+=[\"'][^\"']*[\"']\\s*)*/?>");

/**
* Used to convert entities into strings.
*/
private final DocTrees docTrees;

AbstractAsciidocTreeVisitor(final DocTrees docTrees) {
this.docTrees = docTrees;
}

@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();
data.getCurrentParagraph().setContext(BlockImpl.LISTING_CONTEXT);
break;
case "code":
data.newTextSpan();
break;
case "em":
case "i":
data.newTextSpan();
break;
case "strong":
case "b":
data.newTextSpan();
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 "td":
data.popNode();
break;
case "th":
data.popNode().setContext(CellImpl.HEADER_CONTEXT);
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":
final Block codeBlock = data.newParagraph();
final java.util.List<String> 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<String> 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
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.getBody()) {
idx += row.getCells().size();
}
final Row row = new RowImpl();
for (int i = idx; i < table.getBlocks().size(); i++) {
final StructuralNode cell = cells.get(i);
if (cell instanceof Cell) {
row.getCells().add((Cell) cell);
}
}
table.getBody().add(row);
break;
case "code":
appendSpan(data, CODE_DELIM);
break;
case "em":
case "i":
appendSpan(data, EMPHASIS_DELIM);
break;
case "strong":
case "b":
appendSpan(data, STRONG_EMPHASIS_DELIM);
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) {
data.newTextSpan();
node.getBody().accept(this, data);
appendSpan(data, "`");
} else {
node.getBody().accept(this, data);
}
return super.visitLiteral(node, data);
}

@Override
public Void visitEntity(final EntityTree node, final AsciidocData asciidocData) {
final String text = docTrees.getCharacters(node);
if (text != null) {
asciidocData.append(text);
}
return super.visitEntity(node, asciidocData);
}

@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);
}

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);
}
}
Loading