From 2515affa7fda2ec42c2d927b82d87a39a776be8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Wed, 23 Sep 2026 21:02:18 +0300 Subject: [PATCH 1/3] fix: preserve preformatted help descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Efe Gökdemir --- .../src/it/help-basic-jdk11/expected-help.txt | 6 +++ .../src/main/java/test/MyMojo.java | 12 +++++ .../generator/HtmlToPlainTextConverter.java | 53 +++++++++++++++++-- .../main/resources/help-class-source-v4.vm | 23 ++++++++ .../src/main/resources/help-class-source.vm | 23 ++++++++ .../HtmlToPlainTextConverterTest.java | 9 ++++ 6 files changed, 121 insertions(+), 5 deletions(-) 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..6ae47c866 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,30 @@ 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 + 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 +91,20 @@ 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 (name.equals("pre")) { + 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 +116,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..9946e9c38 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 @@ -400,6 +400,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..5c358ec53 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 @@ -390,6 +390,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..c9b155dcf 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,15 @@ 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)); + } + @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") From d8ca673097adef5da4b6b78166be8ac98d28d7cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Sat, 17 Oct 2026 10:26:57 +0300 Subject: [PATCH 2/3] fix: omit whitespace separator before preformatted help --- .../main/resources/help-class-source-v4.vm | 22 +++++++++++++++++-- .../src/main/resources/help-class-source.vm | 22 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) 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 9946e9c38..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; 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 5c358ec53..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; From 7d3d905ae261dee0e2e52ca8ecf1bed90226325b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Sat, 17 Oct 2026 14:51:25 +0300 Subject: [PATCH 3/3] test: cover preformatted text boundaries --- .../plugin/generator/HtmlToPlainTextConverter.java | 3 +-- .../generator/HtmlToPlainTextConverterTest.java | 12 ++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) 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 6ae47c866..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 @@ -64,6 +64,7 @@ private static class FormattingVisitor implements NodeVisitor { 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
@@ -96,8 +97,6 @@ public void tail(Node node, int depth) {
                 preformatted--;
                 preformattedEndsWithNewline = accum.length() > 0 && accum.charAt(accum.length() - 1) == '\n';
                 accum.append(PRE_END);
-            }
-            if (name.equals("pre")) {
                 if (!preformattedEndsWithNewline) {
                     accum.append('\n');
                 }
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 c9b155dcf..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
@@ -94,6 +94,18 @@ void preformattedTextPreservesWhitespace() {
                 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")