001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.site; 021 022import java.io.IOException; 023import java.nio.file.FileVisitResult; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.nio.file.SimpleFileVisitor; 027import java.nio.file.attribute.BasicFileAttributes; 028import java.util.ArrayList; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.TreeMap; 033import java.util.regex.Matcher; 034import java.util.regex.Pattern; 035 036import javax.annotation.Nullable; 037 038import org.apache.maven.doxia.macro.AbstractMacro; 039import org.apache.maven.doxia.macro.Macro; 040import org.apache.maven.doxia.macro.MacroExecutionException; 041import org.apache.maven.doxia.macro.MacroRequest; 042import org.apache.maven.doxia.sink.Sink; 043import org.codehaus.plexus.component.annotations.Component; 044 045import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 046 047/** 048 * Macro to generate table rows for all Checkstyle modules. 049 * Includes every Check.java file that has a Javadoc. 050 * Uses href path structure based on src/site/xdoc/checks. 051 * Usage: 052 * <pre> 053 * <macro name="allCheckSummaries"/> 054 * </pre> 055 * 056 * <p>Supports optional "package" parameter to filter checks by package. 057 * When package parameter is provided, only checks from that package are included. 058 * Usage: 059 * <pre> 060 * <macro name="allCheckSummaries"> 061 * <param name="package" value="annotation"/> 062 * </macro> 063 * </pre> 064 */ 065@Component(role = Macro.class, hint = "allCheckSummaries") 066public class AllCheckSummaries extends AbstractMacro { 067 068 /** Initial capacity for StringBuilder in wrapSummary method. */ 069 public static final int CAPACITY = 3000; 070 071 /** 072 * Matches common HTML tags such as paragraph, div, span, strong, and em. 073 * Used to remove formatting tags from the Javadoc HTML content. 074 * Note: anchor tags are preserved. 075 */ 076 private static final Pattern TAG_PATTERN = 077 Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>"); 078 079 /** Whitespace regex pattern string. */ 080 private static final String WHITESPACE_REGEX = "\\s+"; 081 082 /** 083 * Matches one or more whitespace characters. 084 * Used to normalize spacing in sanitized text. 085 */ 086 private static final Pattern SPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX); 087 088 /** 089 * Matches '&' characters that are not part of a valid HTML entity. 090 */ 091 private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)"); 092 093 /** 094 * Pattern to match href attributes in anchor tags. 095 * Captures the URL within the href attribute, including any newlines. 096 * DOTALL flag allows . to match newlines, making the pattern work across line breaks. 097 */ 098 private static final Pattern HREF_PATTERN = 099 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("&"); 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}