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. + + https://example.test/plugin.exe + + defaultParam (Default: escape\backslash) This parameter uses "quotation" marks and backslashes '\\' in its description. Those characters must be escaped in Java string literals. diff --git a/maven-plugin-plugin/src/it/help-basic-jdk11/src/main/java/test/MyMojo.java b/maven-plugin-plugin/src/it/help-basic-jdk11/src/main/java/test/MyMojo.java index 480fdbc77..caae655b0 100644 --- a/maven-plugin-plugin/src/it/help-basic-jdk11/src/main/java/test/MyMojo.java +++ b/maven-plugin-plugin/src/it/help-basic-jdk11/src/main/java/test/MyMojo.java @@ -41,6 +41,18 @@ public class MyMojo extends AbstractMojo { @Parameter(defaultValue = "escape\\backslash") private String defaultParam; + /** + * 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
middle
second  
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")