View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import java.io.IOException;
23  import java.nio.file.FileVisitResult;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.SimpleFileVisitor;
27  import java.nio.file.attribute.BasicFileAttributes;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.TreeMap;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  
36  import javax.annotation.Nullable;
37  
38  import org.apache.maven.doxia.macro.AbstractMacro;
39  import org.apache.maven.doxia.macro.Macro;
40  import org.apache.maven.doxia.macro.MacroExecutionException;
41  import org.apache.maven.doxia.macro.MacroRequest;
42  import org.apache.maven.doxia.sink.Sink;
43  import org.codehaus.plexus.component.annotations.Component;
44  
45  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
46  
47  /**
48   * Macro to generate table rows for all Checkstyle modules.
49   * Includes every Check.java file that has a Javadoc.
50   * Uses href path structure based on src/site/xdoc/checks.
51   * Usage:
52   * <pre>
53   * &lt;macro name="allCheckSummaries"/&gt;
54   * </pre>
55   *
56   * <p>Supports optional "package" parameter to filter checks by package.
57   * When package parameter is provided, only checks from that package are included.
58   * Usage:
59   * <pre>
60   * &lt;macro name="allCheckSummaries"&gt;
61   *   &lt;param name="package" value="annotation"/&gt;
62   * &lt;/macro&gt;
63   * </pre>
64   */
65  @Component(role = Macro.class, hint = "allCheckSummaries")
66  public class AllCheckSummaries extends AbstractMacro {
67  
68      /** Initial capacity for StringBuilder in wrapSummary method. */
69      public static final int CAPACITY = 3000;
70  
71      /**
72       * Matches common HTML tags such as paragraph, div, span, strong, and em.
73       * Used to remove formatting tags from the Javadoc HTML content.
74       * Note: anchor tags are preserved.
75       */
76      private static final Pattern TAG_PATTERN =
77              Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>");
78  
79      /** Whitespace regex pattern string. */
80      private static final String WHITESPACE_REGEX = "\\s+";
81  
82      /**
83       * Matches one or more whitespace characters.
84       * Used to normalize spacing in sanitized text.
85       */
86      private static final Pattern SPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX);
87  
88      /**
89       * Matches '&amp;' characters that are not part of a valid HTML entity.
90       */
91      private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)");
92  
93      /**
94       * Pattern to match href attributes in anchor tags.
95       * Captures the URL within the href attribute, including any newlines.
96       * DOTALL flag allows . to match newlines, making the pattern work across line breaks.
97       */
98      private static final Pattern HREF_PATTERN =
99              Pattern.compile("href\\s*=\\s*['\"]([^'\"]*)['\"]",
100                     Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
101 
102     /** Path component for source directory. */
103     private static final String SRC = "src";
104 
105     /** Path component for checks directory. */
106     private static final String CHECKS = "checks";
107 
108     /** Root path for Java check files. */
109     private static final Path JAVA_CHECKS_ROOT = Path.of(
110             SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle", CHECKS);
111 
112     /** Root path for site check XML files. */
113     private static final Path SITE_CHECKS_ROOT = Path.of(SRC, "site", "xdoc", CHECKS);
114 
115     /** XML file extension. */
116     private static final String XML_EXTENSION = ".xml";
117 
118     /** HTML file extension. */
119     private static final String HTML_EXTENSION = ".html";
120 
121     /** TD opening tag. */
122     private static final String TD_TAG = "<td>";
123 
124     /** TD closing tag. */
125     private static final String TD_CLOSE_TAG = "</td>";
126 
127     /** Package name for miscellaneous checks. */
128     private static final String MISC_PACKAGE = "misc";
129 
130     /** Package name for annotation checks. */
131     private static final String ANNOTATION_PACKAGE = "annotation";
132 
133     /** HTML table closing tag. */
134     private static final String TABLE_CLOSE_TAG = "</table>";
135 
136     /** HTML div closing tag. */
137     private static final String DIV_CLOSE_TAG = "</div>";
138 
139     /** HTML section closing tag. */
140     private static final String SECTION_CLOSE_TAG = "</section>";
141 
142     /** HTML div wrapper opening tag. */
143     private static final String DIV_WRAPPER_TAG = "<div class=\"wrapper\">";
144 
145     /** HTML table opening tag. */
146     private static final String TABLE_OPEN_TAG = "<table>";
147 
148     /** HTML anchor separator. */
149     private static final String ANCHOR_SEPARATOR = "#";
150 
151     /** Regex replacement for first capture group. */
152     private static final String FIRST_CAPTURE_GROUP = "$1";
153 
154     /** Maximum line width for complete line including indentation. */
155     private static final int MAX_LINE_WIDTH_TOTAL = 100;
156 
157     /** Indentation width for INDENT_LEVEL_14 (14 spaces). */
158     private static final int INDENT_WIDTH = 14;
159 
160     /** Maximum content width excluding indentation. */
161     private static final int MAX_CONTENT_WIDTH = MAX_LINE_WIDTH_TOTAL - INDENT_WIDTH;
162 
163     /** Closing anchor tag. */
164     private static final String CLOSING_ANCHOR_TAG = "</a>";
165 
166     /** Pattern to match trailing spaces before closing code tags. */
167     private static final Pattern CODE_SPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX
168             + "(" + CLOSING_ANCHOR_TAG.substring(0, 2) + "code>)");
169 
170     @Override
171     public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
172         final String packageFilter = (String) request.getParameter("package");
173 
174         final Map<String, String> xmlHrefMap = buildXmlHtmlMap();
175         final Map<String, CheckInfo> infos = new TreeMap<>();
176 
177         processCheckFiles(infos, xmlHrefMap, packageFilter);
178 
179         final StringBuilder normalRows = new StringBuilder(4096);
180         final StringBuilder holderRows = new StringBuilder(512);
181 
182         buildTableRows(infos, normalRows, holderRows);
183 
184         sink.rawText(normalRows.toString());
185         if (packageFilter == null && !holderRows.isEmpty()) {
186             appendHolderSection(sink, holderRows);
187         }
188         else if (packageFilter != null && !holderRows.isEmpty()) {
189             appendFilteredHolderSection(sink, holderRows, packageFilter);
190         }
191     }
192 
193     /**
194      * Scans Java sources and populates info map with modules having Javadoc.
195      *
196      * @param infos map of collected module info
197      * @param xmlHrefMap map of XML-to-HTML hrefs
198      * @param packageFilter optional package to filter by, null for all
199      * @throws MacroExecutionException if file walk fails
200      */
201     private static void processCheckFiles(Map<String, CheckInfo> infos,
202                                           Map<String, String> xmlHrefMap,
203                                           String packageFilter)
204             throws MacroExecutionException {
205         try {
206             final List<Path> checkFiles = new ArrayList<>();
207             Files.walkFileTree(JAVA_CHECKS_ROOT, new SimpleFileVisitor<>() {
208                 @Override
209                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
210                     if (isCheckOrHolderFile(file)) {
211                         checkFiles.add(file);
212                     }
213                     return FileVisitResult.CONTINUE;
214                 }
215             });
216 
217             checkFiles.forEach(path -> processCheckFile(path, infos, xmlHrefMap, packageFilter));
218         }
219         catch (IOException | IllegalStateException exception) {
220             throw new MacroExecutionException("Failed to discover checks", exception);
221         }
222     }
223 
224     /**
225      * Checks if a path is a Check or Holder Java file.
226      *
227      * @param path the path to check
228      * @return true if the path is a Check or Holder file, false otherwise
229      */
230     private static boolean isCheckOrHolderFile(Path path) {
231         final Path fileNamePath = path.getFileName();
232         boolean isCheckOrHolder = false;
233         if (fileNamePath != null && Files.isRegularFile(path)) {
234             final String fileName = fileNamePath.toString();
235             isCheckOrHolder = (fileName.endsWith("Check.java") || fileName.endsWith("Holder.java"))
236                     && (!fileName.startsWith("Abstract")
237                     || "AbstractClassNameCheck.java".equals(fileName));
238         }
239         return isCheckOrHolder;
240     }
241 
242     /**
243      * Checks if a module is a holder type.
244      *
245      * @param moduleName the module name
246      * @return true if the module is a holder, false otherwise
247      */
248     private static boolean isHolder(String moduleName) {
249         return moduleName.endsWith("Holder");
250     }
251 
252     /**
253      * Processes a single check class file and extracts metadata.
254      *
255      * @param path the check class file
256      * @param infos map of results
257      * @param xmlHrefMap map of XML hrefs
258      * @param packageFilter optional package to filter by, null for all
259      * @throws IllegalArgumentException if macro execution fails
260      */
261     private static void processCheckFile(Path path, Map<String, CheckInfo> infos,
262                                          Map<String, String> xmlHrefMap,
263                                          String packageFilter) {
264         try {
265             final String moduleName = CommonUtil.getFileNameWithoutExtension(path.toString());
266             SiteUtil.processModule(moduleName, path);
267             if (!JavadocScraperResultUtil.getModuleSinceVersion().isEmpty()) {
268                 final String description = JavadocScraperResultUtil.getModuleDescription();
269                 if (!description.isEmpty()) {
270 
271                     final String[] moduleInfo = determineModuleInfo(path, moduleName);
272                     final String packageName = moduleInfo[1];
273                     if (packageFilter == null || packageFilter.equals(packageName)) {
274                         final String sanitizedDescription = sanitizeAnchorUrls(description);
275 
276                         final String simpleName = moduleInfo[0];
277                         final String summary = sanitizeAndFirstSentence(sanitizedDescription);
278                         final String href = resolveHref(xmlHrefMap, packageName, simpleName,
279                                 packageFilter);
280                         infos.put(simpleName, new CheckInfo(simpleName, href, summary));
281                     }
282                 }
283             }
284 
285         }
286         catch (MacroExecutionException exceptionThrown) {
287             throw new IllegalArgumentException(exceptionThrown);
288         }
289     }
290 
291     /**
292      * Determines the simple name and package name for a check module.
293      *
294      * @param path the check class file
295      * @param moduleName the full module name
296      * @return array with [simpleName, packageName]
297      */
298     private static String[] determineModuleInfo(Path path, String moduleName) {
299         String packageName = extractCategoryFromJavaPath(path);
300 
301         if ("indentation".equals(packageName)) {
302             packageName = MISC_PACKAGE;
303         }
304         if (isHolder(moduleName)) {
305             packageName = ANNOTATION_PACKAGE;
306         }
307         final String simpleName;
308         if (isHolder(moduleName)) {
309             simpleName = moduleName;
310         }
311         else {
312             simpleName = moduleName.substring(0, moduleName.length() - "Check".length());
313         }
314 
315         return new String[] {simpleName, packageName};
316     }
317 
318     /**
319      * Builds HTML rows for both normal and holder check modules.
320      *
321      * @param infos map of collected module info
322      * @param normalRows builder for normal check rows
323      * @param holderRows builder for holder check rows
324      */
325     private static void buildTableRows(Map<String, CheckInfo> infos,
326                                        StringBuilder normalRows,
327                                        StringBuilder holderRows) {
328         for (CheckInfo info : infos.values()) {
329             final String row = buildTableRow(info);
330             if (isHolder(info.simpleName())) {
331                 holderRows.append(row);
332             }
333             else {
334                 normalRows.append(row);
335             }
336         }
337         removeLeadingNewline(normalRows);
338         removeLeadingNewline(holderRows);
339     }
340 
341     /**
342      * Builds a single table row for a check module.
343      *
344      * @param info check module information
345      * @return the HTML table row as a string
346      */
347     private static String buildTableRow(CheckInfo info) {
348         final String ind10 = ModuleJavadocParsingUtil.INDENT_LEVEL_10;
349         final String ind12 = ModuleJavadocParsingUtil.INDENT_LEVEL_12;
350         final String ind14 = ModuleJavadocParsingUtil.INDENT_LEVEL_14;
351         final String ind16 = ModuleJavadocParsingUtil.INDENT_LEVEL_16;
352 
353         final String cleanSummary = sanitizeAnchorUrls(info.summary());
354 
355         return ind10 + "<tr>"
356                 + ind12 + TD_TAG
357                 + ind14
358                 + "<a href=\""
359                 + info.link()
360                 + "\">"
361                 + ind16 + info.simpleName()
362                 + ind14 + CLOSING_ANCHOR_TAG
363                 + ind12 + TD_CLOSE_TAG
364                 + ind12 + TD_TAG
365                 + ind14 + wrapSummary(cleanSummary)
366                 + ind12 + TD_CLOSE_TAG
367                 + ind10 + "</tr>";
368     }
369 
370     /**
371      * Removes leading newline characters from a StringBuilder.
372      *
373      * @param builder the StringBuilder to process
374      */
375     private static void removeLeadingNewline(StringBuilder builder) {
376         while (!builder.isEmpty() && Character.isWhitespace(builder.charAt(0))) {
377             builder.delete(0, 1);
378         }
379     }
380 
381     /**
382      * Appends the Holder Checks HTML section.
383      *
384      * @param sink the output sink
385      * @param holderRows the holder rows content
386      */
387     private static void appendHolderSection(Sink sink, StringBuilder holderRows) {
388         final String holderSection = ModuleJavadocParsingUtil.INDENT_LEVEL_8
389                 + TABLE_CLOSE_TAG
390                 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
391                 + DIV_CLOSE_TAG
392                 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
393                 + SECTION_CLOSE_TAG
394                 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
395                 + "<section name=\"Holder Checks\">"
396                 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
397                 + "<p>"
398                 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
399                 + "These checks aren't normal checks and are usually"
400                 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
401                 + "associated with a specialized filter to gather"
402                 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
403                 + "information the filter can't get on its own."
404                 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
405                 + "</p>"
406                 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
407                 + DIV_WRAPPER_TAG
408                 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
409                 + TABLE_OPEN_TAG
410                 + ModuleJavadocParsingUtil.INDENT_LEVEL_10
411                 + holderRows;
412         sink.rawText(holderSection);
413     }
414 
415     /**
416      * Appends the filtered Holder Checks section for package views.
417      *
418      * @param sink the output sink
419      * @param holderRows the holder rows content
420      * @param packageName the package name
421      */
422     private static void appendFilteredHolderSection(Sink sink, StringBuilder holderRows,
423                                                     String packageName) {
424         final String packageTitle = getPackageDisplayName(packageName);
425         final String holderSection = ModuleJavadocParsingUtil.INDENT_LEVEL_8
426                 + TABLE_CLOSE_TAG
427                 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
428                 + DIV_CLOSE_TAG
429                 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
430                 + SECTION_CLOSE_TAG
431                 + ModuleJavadocParsingUtil.INDENT_LEVEL_4
432                 + "<section name=\"" + packageTitle + " Holder Checks\">"
433                 + ModuleJavadocParsingUtil.INDENT_LEVEL_6
434                 + DIV_WRAPPER_TAG
435                 + ModuleJavadocParsingUtil.INDENT_LEVEL_8
436                 + TABLE_OPEN_TAG
437                 + ModuleJavadocParsingUtil.INDENT_LEVEL_10
438                 + holderRows;
439         sink.rawText(holderSection);
440     }
441 
442     /**
443      * Get display name for package (capitalize first letter).
444      *
445      * @param packageName the package name
446      * @return the capitalized package name
447      */
448     private static String getPackageDisplayName(String packageName) {
449         final String result;
450         if (packageName == null || packageName.isEmpty()) {
451             result = packageName;
452         }
453         else {
454             result = packageName.substring(0, 1).toUpperCase(Locale.ENGLISH)
455                     + packageName.substring(1);
456         }
457         return result;
458     }
459 
460     /**
461      * Builds map of XML file names to HTML documentation paths.
462      *
463      * @return map of lowercase check names to hrefs
464      */
465     private static Map<String, String> buildXmlHtmlMap() {
466         final Map<String, String> map = new TreeMap<>();
467         if (Files.exists(SITE_CHECKS_ROOT)) {
468             try {
469                 final List<Path> xmlFiles = new ArrayList<>();
470                 Files.walkFileTree(SITE_CHECKS_ROOT, new SimpleFileVisitor<>() {
471                     @Override
472                     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
473                         if (isValidXmlFile(file)) {
474                             xmlFiles.add(file);
475                         }
476                         return FileVisitResult.CONTINUE;
477                     }
478                 });
479 
480                 xmlFiles.forEach(path -> addXmlHtmlMapping(path, map));
481             }
482             catch (IOException ignored) {
483                 // ignore
484             }
485         }
486         return map;
487     }
488 
489     /**
490      * Checks if a path is a valid XML file for processing.
491      *
492      * @param path the path to check
493      * @return true if the path is a valid XML file, false otherwise
494      */
495     private static boolean isValidXmlFile(Path path) {
496         final Path fileName = path.getFileName();
497         return fileName != null
498                 && !("index" + XML_EXTENSION).equalsIgnoreCase(fileName.toString())
499                 && path.toString().endsWith(XML_EXTENSION)
500                 && Files.isRegularFile(path);
501     }
502 
503     /**
504      * Adds XML-to-HTML mapping entry to map.
505      *
506      * @param path the XML file path
507      * @param map the mapping to update
508      */
509     private static void addXmlHtmlMapping(Path path, Map<String, String> map) {
510         final Path fileName = path.getFileName();
511         if (fileName != null) {
512             final String fileNameString = fileName.toString();
513             final int extensionLength = 4;
514             final String base = fileNameString.substring(0,
515                             fileNameString.length() - extensionLength)
516                     .toLowerCase(Locale.ROOT);
517             final Path relativePath = SITE_CHECKS_ROOT.relativize(path);
518             final String relativePathString = relativePath.toString();
519             final String rel = relativePathString
520                     .replace('\\', '/')
521                     .replace(XML_EXTENSION, HTML_EXTENSION);
522             map.put(base, CHECKS + "/" + rel);
523         }
524     }
525 
526     /**
527      * Resolves the href for a given check module.
528      * When packageFilter is null, returns full path: checks/category/filename.html#CheckName
529      * When packageFilter is set, returns relative path: filename.html#CheckName
530      *
531      * @param xmlMap map of XML file names to HTML paths
532      * @param category the category of the check
533      * @param simpleName simple name of the check
534      * @param packageFilter optional package filter, null for all checks
535      * @return the resolved href for the check
536      */
537     private static String resolveHref(Map<String, String> xmlMap, String category,
538                                       String simpleName, @Nullable String packageFilter) {
539         final String lower = simpleName.toLowerCase(Locale.ROOT);
540         final String href = xmlMap.get(lower);
541         final String result;
542 
543         if (href != null) {
544             if (packageFilter == null) {
545                 result = href + ANCHOR_SEPARATOR + simpleName;
546             }
547             else {
548                 final int lastSlash = href.lastIndexOf('/');
549                 final String filename;
550                 if (lastSlash >= 0) {
551                     filename = href.substring(lastSlash + 1);
552                 }
553                 else {
554                     filename = href;
555                 }
556                 result = filename + ANCHOR_SEPARATOR + simpleName;
557             }
558         }
559         else {
560             if (packageFilter == null) {
561                 result = String.format(Locale.ROOT, "%s/%s/%s.html%s%s",
562                         CHECKS, category, lower, ANCHOR_SEPARATOR, simpleName);
563             }
564             else {
565                 result = String.format(Locale.ROOT, "%s.html%s%s",
566                         lower, ANCHOR_SEPARATOR, simpleName);
567             }
568         }
569         return result;
570     }
571 
572     /**
573      * Extracts category path from a Java file path.
574      *
575      * @param javaPath the Java source file path
576      * @return the category path extracted from the Java path
577      */
578     private static String extractCategoryFromJavaPath(Path javaPath) {
579         final Path rel = JAVA_CHECKS_ROOT.relativize(javaPath);
580         final Path parent = rel.getParent();
581         final String result;
582         if (parent == null) {
583             result = MISC_PACKAGE;
584         }
585         else {
586             result = parent.toString().replace('\\', '/');
587         }
588         return result;
589     }
590 
591     /**
592      * Sanitizes URLs within anchor tags by removing whitespace from href attributes.
593      *
594      * @param html the HTML string containing anchor tags
595      * @return the HTML with sanitized URLs
596      */
597     private static String sanitizeAnchorUrls(String html) {
598         final String result;
599         if (html == null || html.isEmpty()) {
600             result = html;
601         }
602         else {
603             final Matcher matcher = HREF_PATTERN.matcher(html);
604             final StringBuilder buffer = new StringBuilder(html.length());
605 
606             while (matcher.find()) {
607                 final String originalUrl = matcher.group(1);
608                 final String cleanedUrl = SPACE_PATTERN.matcher(originalUrl).replaceAll("");
609                 final String replacement = "href=\""
610                         + Matcher.quoteReplacement(cleanedUrl) + "\"";
611                 matcher.appendReplacement(buffer, replacement);
612             }
613             matcher.appendTail(buffer);
614 
615             result = buffer.toString();
616         }
617         return result;
618     }
619 
620     /**
621      * Sanitizes HTML and extracts first sentence.
622      * Preserves anchor tags while removing other HTML formatting.
623      * Also cleans whitespace from URLs in href attributes.
624      *
625      * @param html the HTML string to process
626      * @return the sanitized first sentence
627      */
628     private static String sanitizeAndFirstSentence(String html) {
629         final String result;
630         if (html == null || html.isEmpty()) {
631             result = "";
632         }
633         else {
634             String cleaned = sanitizeAnchorUrls(html);
635             cleaned = TAG_PATTERN.matcher(cleaned).replaceAll("");
636             cleaned = SPACE_PATTERN.matcher(cleaned).replaceAll(" ").trim();
637             cleaned = AMP_PATTERN.matcher(cleaned).replaceAll("&amp;");
638             cleaned = CODE_SPACE_PATTERN.matcher(cleaned).replaceAll(FIRST_CAPTURE_GROUP);
639             result = extractFirstSentence(cleaned);
640         }
641         return result;
642     }
643 
644     /**
645      * Extracts first sentence from plain text.
646      *
647      * @param text the text to process
648      * @return the first sentence extracted from the text
649      */
650     private static String extractFirstSentence(String text) {
651         String result = "";
652         if (text != null && !text.isEmpty()) {
653             int end = -1;
654             for (int index = 0; index < text.length(); index++) {
655                 if (text.charAt(index) == '.'
656                         && (index == text.length() - 1
657                         || Character.isWhitespace(text.charAt(index + 1))
658                         || text.charAt(index + 1) == '<')) {
659                     end = index;
660                     break;
661                 }
662             }
663             if (end == -1) {
664                 result = text.trim();
665             }
666             else {
667                 result = text.substring(0, end + 1).trim();
668             }
669         }
670         return result;
671     }
672 
673     /**
674      * Wraps long summaries to avoid exceeding line width.
675      * Preserves URLs in anchor tags by breaking after the opening tag's closing bracket.
676      *
677      * @param text the text to wrap
678      * @return the wrapped text
679      */
680     private static String wrapSummary(String text) {
681         String wrapped = "";
682 
683         if (text != null && !text.isEmpty()) {
684             final String sanitized = sanitizeAnchorUrls(text);
685 
686             final String indent = ModuleJavadocParsingUtil.INDENT_LEVEL_14;
687             final String clean = sanitized.trim();
688 
689             final StringBuilder result = new StringBuilder(CAPACITY);
690             int cleanIndex = 0;
691             final int cleanLen = clean.length();
692 
693             while (cleanIndex < cleanLen) {
694                 final int remainingChars = cleanLen - cleanIndex;
695 
696                 if (remainingChars <= MAX_CONTENT_WIDTH) {
697                     result.append(indent)
698                             .append(clean.substring(cleanIndex))
699                             .append('\n');
700                     break;
701                 }
702 
703                 final int idealBreak = cleanIndex + MAX_CONTENT_WIDTH;
704                 final int actualBreak = calculateBreakPoint(clean, cleanIndex, idealBreak);
705 
706                 result.append(indent)
707                         .append(clean, cleanIndex, actualBreak);
708 
709                 cleanIndex = actualBreak;
710                 while (cleanIndex < cleanLen && clean.charAt(cleanIndex) == ' ') {
711                     cleanIndex++;
712                 }
713             }
714 
715             wrapped = result.toString().trim();
716         }
717 
718         return wrapped;
719     }
720 
721     /**
722      * Calculates the appropriate break point for text wrapping.
723      * Handles anchor tags specially to avoid breaking URLs.
724      *
725      * @param clean the cleaned text to process
726      * @param cleanIndex current position in text
727      * @param idealBreak ideal break position
728      * @return the actual break position
729      */
730     private static int calculateBreakPoint(String clean, int cleanIndex, int idealBreak) {
731         final int anchorStart = clean.indexOf("<a ", cleanIndex);
732         final int anchorOpenEnd;
733         if (anchorStart == -1) {
734             anchorOpenEnd = -1;
735         }
736         else {
737             anchorOpenEnd = clean.indexOf('>', anchorStart);
738         }
739 
740         final int actualBreak;
741         if (shouldBreakAfterAnchorOpen(anchorStart, anchorOpenEnd, idealBreak)) {
742             actualBreak = anchorOpenEnd + 1;
743         }
744         else if (shouldBreakAfterAnchorContent(anchorStart, anchorOpenEnd,
745                 idealBreak, clean)) {
746             actualBreak = anchorOpenEnd + 1;
747         }
748         else {
749             actualBreak = findSafeBreakPoint(clean, cleanIndex, idealBreak);
750         }
751 
752         return actualBreak;
753     }
754 
755     /**
756      * Determines if break should occur after anchor opening tag.
757      *
758      * @param anchorStart start position of anchor tag
759      * @param anchorOpenEnd end position of anchor opening tag
760      * @param idealBreak ideal break position
761      * @return true if should break after anchor opening
762      */
763     private static boolean shouldBreakAfterAnchorOpen(int anchorStart, int anchorOpenEnd,
764                                                       int idealBreak) {
765         return anchorStart != -1 && anchorStart < idealBreak
766                 && anchorOpenEnd != -1 && anchorOpenEnd >= idealBreak;
767     }
768 
769     /**
770      * Determines if break should occur after anchor content.
771      *
772      * @param anchorStart start position of anchor tag
773      * @param anchorOpenEnd end position of anchor opening tag
774      * @param idealBreak ideal break position
775      * @param clean the text being processed
776      * @return true if should break after anchor content
777      */
778     private static boolean shouldBreakAfterAnchorContent(int anchorStart, int anchorOpenEnd,
779                                                          int idealBreak, String clean) {
780         final boolean result;
781         if (anchorStart != -1 && anchorStart < idealBreak
782                 && anchorOpenEnd != -1 && anchorOpenEnd < idealBreak) {
783             final int anchorCloseStart = clean.indexOf(CLOSING_ANCHOR_TAG, anchorOpenEnd);
784             result = anchorCloseStart != -1 && anchorCloseStart >= idealBreak;
785         }
786         else {
787             result = false;
788         }
789         return result;
790     }
791 
792     /**
793      * Finds a safe break point at a space character.
794      *
795      * @param text the text to search
796      * @param start the start index
797      * @param idealBreak the ideal break position
798      * @return the actual break position
799      */
800     private static int findSafeBreakPoint(String text, int start, int idealBreak) {
801         final int actualBreak;
802         final int lastSpace = text.lastIndexOf(' ', idealBreak);
803 
804         if (lastSpace > start && lastSpace >= start + MAX_CONTENT_WIDTH / 2) {
805             actualBreak = lastSpace;
806         }
807         else {
808             actualBreak = idealBreak;
809         }
810 
811         return actualBreak;
812     }
813 
814     /**
815      * Data holder for each Check module entry.
816      *
817      * @param simpleName check simple name
818      * @param link documentation link
819      * @param summary module summary
820      */
821     private record CheckInfo(String simpleName, String link, String summary) {
822     }
823 }