diff --git a/maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt b/maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt
index 069f241b3..00577282b 100644
--- a/maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt
+++ b/maven-plugin-plugin/src/it/help-basic-jdk11/expected-help.txt
@@ -10,6 +10,12 @@ help-jdk11:test
Available parameters:
+ codeExample
+ A parameter with a preformatted example.
+
{@code
+ *
+ * https://example.test/plugin.exe
+ *
+ * }
+ */
+ @Parameter
+ private String codeExample;
+
/**
* This parameter is deprecated.
*
diff --git a/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/HtmlToPlainTextConverter.java b/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/HtmlToPlainTextConverter.java
index 6906c75cf..de840f8f7 100644
--- a/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/HtmlToPlainTextConverter.java
+++ b/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/HtmlToPlainTextConverter.java
@@ -59,17 +59,31 @@ private String getPlainText(Element element) {
// the formatting rules, implemented in a breadth-first DOM traverse
private static class FormattingVisitor implements NodeVisitor {
+ private static final char PRE_START = '\u0001';
+ private static final char PRE_END = '\u0002';
+
private StringBuilder accum = new StringBuilder(); // holds the accumulated text
+ // Nested elements are invalid HTML; Jsoup normalises them before traversal.
+ private int preformatted;
+
// hit when the node is first seen
public void head(Node node, int depth) {
String name = node.nodeName();
if (node instanceof TextNode) {
- accum.append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM.
+ TextNode textNode = (TextNode) node;
+ // TextNode#text() normalizes whitespace. Keep the original text inside preformatted blocks.
+ accum.append(preformatted > 0 ? textNode.getWholeText() : textNode.text());
} else if (name.equals("li")) {
accum.append("\n * ");
} else if (name.equals("dt")) {
accum.append(" ");
+ } else if (name.equals("pre")) {
+ if (accum.length() > 0 && accum.charAt(accum.length() - 1) != '\n') {
+ accum.append('\n');
+ }
+ accum.append(PRE_START);
+ preformatted++;
} else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5", "tr")) {
accum.append("\n");
}
@@ -78,8 +92,18 @@ public void head(Node node, int depth) {
// hit when all of the node's children (if any) have been visited
public void tail(Node node, int depth) {
String name = node.nodeName();
- if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5")) {
- accum.append("\n");
+ boolean preformattedEndsWithNewline = false;
+ if (name.equals("pre")) {
+ preformatted--;
+ preformattedEndsWithNewline = accum.length() > 0 && accum.charAt(accum.length() - 1) == '\n';
+ accum.append(PRE_END);
+ if (!preformattedEndsWithNewline) {
+ accum.append('\n');
+ }
+ } else if (StringUtil.in(name, "br", "dd", "dt", "p", "h1", "h2", "h3", "h4", "h5")) {
+ if (accum.length() == 0 || accum.charAt(accum.length() - 1) != '\n') {
+ accum.append("\n");
+ }
} else if (name.equals("a")) {
// link is empty if it cannot be made absolute
String link = node.absUrl("href");
@@ -91,8 +115,26 @@ public void tail(Node node, int depth) {
@Override
public String toString() {
- // collate multiple consecutive spaces
- return accum.toString().replaceAll(" +", " ").replace("\n ", "\n");
+ String text = accum.toString();
+ StringBuilder result = new StringBuilder(text.length());
+ int start = 0;
+ int preStart;
+ while ((preStart = text.indexOf(PRE_START, start)) >= 0) {
+ appendNormalText(result, text.substring(start, preStart));
+ int preEnd = text.indexOf(PRE_END, preStart);
+ if (preEnd < 0) {
+ appendNormalText(result, text.substring(preStart + 1));
+ return result.toString();
+ }
+ result.append(text, preStart + 1, preEnd);
+ start = preEnd + 1;
+ }
+ appendNormalText(result, text.substring(start));
+ return result.toString();
+ }
+
+ private static void appendNormalText(StringBuilder result, String text) {
+ result.append(text.replaceAll(" +", " ").replace("\n ", "\n"));
}
}
}
diff --git a/maven-plugin-tools-generators/src/main/resources/help-class-source-v4.vm b/maven-plugin-tools-generators/src/main/resources/help-class-source-v4.vm
index 5cb0bb170..9db5cf8ac 100644
--- a/maven-plugin-tools-generators/src/main/resources/help-class-source-v4.vm
+++ b/maven-plugin-tools-generators/src/main/resources/help-class-source-v4.vm
@@ -379,9 +379,27 @@ public class HelpMojo
String[] plainLines = text.split( "(\r\n)|(\r)|(\n)" );
- for ( String plainLine : plainLines )
+ for ( int i = 0; i < plainLines.length; i++ )
{
- toLines( lines, ind + plainLine, indentSize, lineLength );
+ String plainLine = plainLines[i];
+ if ( plainLine.trim().isEmpty() )
+ {
+ boolean previousLineIsPreformatted = i > 0
+ && !plainLines[i - 1].trim().isEmpty()
+ && plainLines[i - 1].startsWith( " " );
+ boolean nextLineIsPreformatted = i + 1 < plainLines.length
+ && !plainLines[i + 1].trim().isEmpty()
+ && plainLines[i + 1].startsWith( " " );
+ // The whitespace-only line before a preformatted block is a boundary, not displayed content.
+ if ( !nextLineIsPreformatted || previousLineIsPreformatted )
+ {
+ lines.add( "" );
+ }
+ }
+ else
+ {
+ toLines( lines, ind + plainLine, indentSize, lineLength );
+ }
}
return lines;
@@ -400,6 +418,29 @@ public class HelpMojo
int lineIndent = getIndentLevel( line );
StringBuilder buf = new StringBuilder( 256 );
+ // Preserve leading spaces in preformatted descriptions instead of treating them as word separators.
+ if ( lineIndent < line.length() && line.charAt( lineIndent ) == ' ' )
+ {
+ for ( int j = 0; j < line.length(); j++ )
+ {
+ char c = line.charAt( j );
+ if ( c == '\t' )
+ {
+ buf.append( repeat( " ", indentSize - buf.length() % indentSize ) );
+ }
+ else if ( c == '\u00A0' )
+ {
+ buf.append( ' ' );
+ }
+ else
+ {
+ buf.append( c );
+ }
+ }
+ lines.add( buf.toString() );
+ return;
+ }
+
String[] tokens = line.split( " +" );
for ( String token : tokens )
diff --git a/maven-plugin-tools-generators/src/main/resources/help-class-source.vm b/maven-plugin-tools-generators/src/main/resources/help-class-source.vm
index 794ed18a1..da82a4095 100644
--- a/maven-plugin-tools-generators/src/main/resources/help-class-source.vm
+++ b/maven-plugin-tools-generators/src/main/resources/help-class-source.vm
@@ -369,9 +369,27 @@ public class HelpMojo
String[] plainLines = text.split( "(\r\n)|(\r)|(\n)" );
- for ( String plainLine : plainLines )
+ for ( int i = 0; i < plainLines.length; i++ )
{
- toLines( lines, ind + plainLine, indentSize, lineLength );
+ String plainLine = plainLines[i];
+ if ( plainLine.trim().isEmpty() )
+ {
+ boolean previousLineIsPreformatted = i > 0
+ && !plainLines[i - 1].trim().isEmpty()
+ && plainLines[i - 1].startsWith( " " );
+ boolean nextLineIsPreformatted = i + 1 < plainLines.length
+ && !plainLines[i + 1].trim().isEmpty()
+ && plainLines[i + 1].startsWith( " " );
+ // The whitespace-only line before a preformatted block is a boundary, not displayed content.
+ if ( !nextLineIsPreformatted || previousLineIsPreformatted )
+ {
+ lines.add( "" );
+ }
+ }
+ else
+ {
+ toLines( lines, ind + plainLine, indentSize, lineLength );
+ }
}
return lines;
@@ -390,6 +408,29 @@ public class HelpMojo
int lineIndent = getIndentLevel( line );
StringBuilder buf = new StringBuilder( 256 );
+ // Preserve leading spaces in preformatted descriptions instead of treating them as word separators.
+ if ( lineIndent < line.length() && line.charAt( lineIndent ) == ' ' )
+ {
+ for ( int j = 0; j < line.length(); j++ )
+ {
+ char c = line.charAt( j );
+ if ( c == '\t' )
+ {
+ buf.append( repeat( " ", indentSize - buf.length() % indentSize ) );
+ }
+ else if ( c == '\u00A0' )
+ {
+ buf.append( ' ' );
+ }
+ else
+ {
+ buf.append( c );
+ }
+ }
+ lines.add( buf.toString() );
+ return;
+ }
+
String[] tokens = line.split( " +" );
for ( String token : tokens )
diff --git a/maven-plugin-tools-generators/src/test/java/org/apache/maven/tools/plugin/generator/HtmlToPlainTextConverterTest.java b/maven-plugin-tools-generators/src/test/java/org/apache/maven/tools/plugin/generator/HtmlToPlainTextConverterTest.java
index 1cbe3e603..95584ce52 100644
--- a/maven-plugin-tools-generators/src/test/java/org/apache/maven/tools/plugin/generator/HtmlToPlainTextConverterTest.java
+++ b/maven-plugin-tools-generators/src/test/java/org/apache/maven/tools/plugin/generator/HtmlToPlainTextConverterTest.java
@@ -85,6 +85,27 @@ void explicitNewline() {
converter.convert(test));
}
+ @Test
+ void preformattedTextPreservesWhitespace() {
+ String test =
+ "For example:\n <binary>\n <url>https://example.test/plugin.exe</url>\n </binary>\n
";
+ assertEquals(
+ "For example:\n \n https://example.test/plugin.exe \n \n",
+ converter.convert(test));
+ }
+
+ @Test
+ void preformattedTextWithoutTrailingNewlinePreservesWhitespace() {
+ assertEquals("before\n first\n second\nafter", converter.convert("before first\n second
after"));
+ }
+
+ @Test
+ void multiplePreformattedBlocksPreserveTheirBoundaries() {
+ assertEquals(
+ "one\n first\nmiddle\nsecond \nend",
+ converter.convert("one first
middlesecond
end"));
+ }
+
@ParameterizedTest(name = "{0} to {1}") // With JUnit 6.0.0 the non-printable chars will be kept in display, see
// https://docs.junit.org/current/user-guide/#writing-tests-parameterized-tests-display-names-quoted-text
@MethodSource("provideConvertParamsBreakLines")