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 00b4fbe8..609cca7d 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 @@ -43,18 +43,25 @@ import org.asciidoctor.ast.StructuralNode; import org.asciidoctor.ast.Table; -class AbstractAsciidocTreeVisitor extends SimpleDocTreeVisitor { +abstract class AbstractAsciidocTreeVisitor extends SimpleDocTreeVisitor { + + private static final String SOURCE_STYLE = "source"; + // These are not supported by AsciiDoctor and are only used internally + private static final String CODE_STYLE = "code"; + private static final String CODE_DELIM = "`"; + private static final String EMPHASIS_STYLE = "em"; + private static final String EMPHASIS_DELIM = "_"; + private static final String STRONG_EMPHASIS_STYLE = "strong"; + private static final String STRONG_EMPHASIS_DELIM = "*"; private static void appendSentences(final String text, final AsciidocData data) { - final String body = StringUtils.normalizeSpace(text); - final String[] sentences = body.split("(?<=\\w{2}[.!?])", -1); + final String[] sentences = text.split("(?<=\\w{2}[.!?])", -1); // Full sentences for (int i = 0; i < sentences.length - 1; i++) { - data.appendWords(sentences[i].strip()); - data.newLine(); + data.appendAdjustingSpace(sentences[i]).newLine(); } // Partial sentence - data.appendWords(sentences[sentences.length - 1].strip()); + data.appendAdjustingSpace(sentences[sentences.length - 1]); } @Override @@ -112,9 +119,19 @@ public Void visitStartElement(final StartElementTree node, final AsciidocData da break; case "pre": data.newParagraph(); - final Block currentParagraph = data.getCurrentParagraph(); - currentParagraph.setContext(BlockImpl.LISTING_CONTEXT); - currentParagraph.setStyle(BlockImpl.SOURCE_STYLE); + data.getCurrentParagraph().setContext(BlockImpl.LISTING_CONTEXT); + data.getCurrentParagraph().setStyle(SOURCE_STYLE); + break; + case "code": + data.newTextSpan(CODE_STYLE); + break; + case "em": + case "i": + data.newTextSpan(EMPHASIS_STYLE); + break; + case "strong": + case "b": + data.newTextSpan(STRONG_EMPHASIS_STYLE); break; default: } @@ -187,6 +204,17 @@ public Void visitEndElement(final EndElementTree node, final AsciidocData data) 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); @@ -210,16 +238,27 @@ public Void visitLink(final LinkTree node, final AsciidocData 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("`"); + data.newTextSpan(CODE_STYLE); + node.getBody().accept(this, data); + appendSpan(data, "`"); } else { node.getBody().accept(this, 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); + } + data.append(delimiter); + } + @Override public Void visitText(final TextTree node, final AsciidocData data) { final Block currentParagraph = data.getCurrentParagraph(); 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 09cbe7a6..a254cb8a 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 @@ -16,8 +16,13 @@ */ package org.apache.logging.log4j.docgen.processor; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; import java.util.EmptyStackException; +import java.util.List; import java.util.function.Function; +import java.util.regex.Pattern; 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; @@ -26,35 +31,41 @@ import org.asciidoctor.ast.StructuralNode; final class AsciidocData { + private static final Pattern WHITESPACE_SEQUENCE = Pattern.compile("\\s+"); + private static final String SPACE = " "; + private static final char SPACE_CHAR = ' '; + private static final char CODE_CHAR = '`'; + private static final String NEW_LINE = "\n"; + private final Document document; private int currentSectionLevel; private StructuralNode currentNode; - // not attached to the current node - private Block currentParagraph; - private final StringBuilder currentLine; + // A stack of nested text blocks. Each can have a different style. + private final Deque paragraphs = new ArrayDeque<>(); + private final Deque lines = new ArrayDeque<>(); public AsciidocData() { document = new DocumentImpl(); currentSectionLevel = 1; currentNode = document; - currentParagraph = new BlockImpl(currentNode); - currentLine = new StringBuilder(); + paragraphs.push(new BlockImpl(currentNode)); + lines.push(new StringBuilder()); } public void newLine() { // Remove trailing space - final String line = currentLine.toString().stripTrailing(); + final String line = getCurrentLine().toString().stripTrailing(); // Ignore leading empty lines - if (!currentParagraph.getLines().isEmpty() || !line.isEmpty()) { - currentParagraph.getLines().add(line); + if (!getCurrentParagraph().getLines().isEmpty() || !line.isEmpty()) { + getCurrentParagraph().getLines().add(line); } - currentLine.setLength(0); + getCurrentLine().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]); + getCurrentLine().append(lines[i]); if (i != lines.length - 1) { newLine(); } @@ -62,19 +73,48 @@ public AsciidocData append(final String text) { return this; } - public void appendWords(final String words) { - if (words.isBlank()) { - return; + public AsciidocData appendAdjustingSpace(final CharSequence text) { + final String normalized = WHITESPACE_SEQUENCE.matcher(text).replaceAll(SPACE); + if (!normalized.isEmpty()) { + final StringBuilder currentLine = getCurrentLine(); + // Last char of current line or space + final char lineLastChar = currentLine.isEmpty() ? SPACE_CHAR : currentLine.charAt(currentLine.length() - 1); + // First char of test + final char textFirstChar = normalized.charAt(0); + if (lineLastChar == SPACE_CHAR && textFirstChar == SPACE_CHAR) { + // Merge spaces + currentLine.append(normalized, 1, normalized.length()); + } else if (lineLastChar == CODE_CHAR && Character.isAlphabetic(textFirstChar)) { + currentLine.append(SPACE_CHAR).append(normalized); + } else { + currentLine.append(normalized); + } } - // Separate text from previous words - if (!currentLine.isEmpty() && Character.isAlphabetic(words.codePointAt(0))) { - currentLine.append(" "); + return this; + } + + public void newTextSpan(final String style) { + paragraphs.push(new BlockImpl(paragraphs.peek())); + lines.push(new StringBuilder()); + } + + public String popTextSpan() { + // Flush the paragraph + final StringBuilder line = lines.peek(); + if (line != null && !line.isEmpty()) { + newLine(); } - currentLine.append(words); + lines.pop(); + return String.join(SPACE, paragraphs.pop().getLines()); } public void newParagraph() { + newParagraph(currentNode); + } + + private void newParagraph(final StructuralNode parent) { newLine(); + final Block currentParagraph = paragraphs.pop(); final java.util.List lines = currentParagraph.getLines(); // Remove trailing empty lines for (int i = lines.size() - 1; i >= 0; i--) { @@ -84,8 +124,8 @@ public void newParagraph() { } if (!currentParagraph.getLines().isEmpty()) { currentNode.append(currentParagraph); - currentParagraph = new BlockImpl(currentNode); } + paragraphs.push(new BlockImpl(parent)); } public StructuralNode getCurrentNode() { @@ -93,11 +133,11 @@ public StructuralNode getCurrentNode() { } public Block getCurrentParagraph() { - return currentParagraph; + return paragraphs.peek(); } public StringBuilder getCurrentLine() { - return currentLine; + return lines.peek(); } public Document getDocument() { @@ -121,12 +161,10 @@ public void setCurrentSectionLevel(final int sectionLevel) { * @param supplier a function to create a new node that takes its parent node a parameter. */ public StructuralNode pushChildNode(final Function supplier) { - // Flushes the current paragraph - newParagraph(); - final StructuralNode child = supplier.apply(currentNode); - // Creates a new current paragraph - currentParagraph = new BlockImpl(child); + + // Flushes and reparents the current paragraph + newParagraph(child); currentNode.append(child); return currentNode = child; @@ -134,15 +172,13 @@ public StructuralNode pushChildNode(final Function lines = new ArrayList<>(); diff --git a/log4j-docgen/src/test/it/example/JavadocExample.java b/log4j-docgen/src/test/it/example/JavadocExample.java index 5ddf2c4c..bc76bfd0 100644 --- a/log4j-docgen/src/test/it/example/JavadocExample.java +++ b/log4j-docgen/src/test/it/example/JavadocExample.java @@ -23,6 +23,13 @@ * paragraph has two sentences. *

*

+ * A sentence with foo, foo`, foobar. Another sentence with {@code foo}, + * {@code foo`}, {@code 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. 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.