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 /** 171 * Creates a new {@code AllCheckSummaries} instance. 172 */ 173 public AllCheckSummaries() { 174 // no code by default 175 } 176 177 @Override 178 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 179 final String packageFilter = (String) request.getParameter("package"); 180 181 final Map<String, String> xmlHrefMap = buildXmlHtmlMap(); 182 final Map<String, CheckInfo> infos = new TreeMap<>(); 183 184 processCheckFiles(infos, xmlHrefMap, packageFilter); 185 186 final StringBuilder normalRows = new StringBuilder(4096); 187 final StringBuilder holderRows = new StringBuilder(512); 188 189 buildTableRows(infos, normalRows, holderRows); 190 191 sink.rawText(normalRows.toString()); 192 if (packageFilter == null && !holderRows.isEmpty()) { 193 appendHolderSection(sink, holderRows); 194 } 195 else if (packageFilter != null && !holderRows.isEmpty()) { 196 appendFilteredHolderSection(sink, holderRows, packageFilter); 197 } 198 } 199 200 /** 201 * Scans Java sources and populates info map with modules having Javadoc. 202 * 203 * @param infos map of collected module info 204 * @param xmlHrefMap map of XML-to-HTML hrefs 205 * @param packageFilter optional package to filter by, null for all 206 * @throws MacroExecutionException if file walk fails 207 */ 208 private static void processCheckFiles(Map<String, CheckInfo> infos, 209 Map<String, String> xmlHrefMap, 210 String packageFilter) 211 throws MacroExecutionException { 212 try { 213 final List<Path> checkFiles = new ArrayList<>(); 214 Files.walkFileTree(JAVA_CHECKS_ROOT, new SimpleFileVisitor<>() { 215 @Override 216 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 217 if (isCheckOrHolderFile(file)) { 218 checkFiles.add(file); 219 } 220 return FileVisitResult.CONTINUE; 221 } 222 }); 223 224 checkFiles.forEach(path -> processCheckFile(path, infos, xmlHrefMap, packageFilter)); 225 } 226 catch (IOException | IllegalStateException exception) { 227 throw new MacroExecutionException("Failed to discover checks", exception); 228 } 229 } 230 231 /** 232 * Checks if a path is a Check or Holder Java file. 233 * 234 * @param path the path to check 235 * @return true if the path is a Check or Holder file, false otherwise 236 */ 237 private static boolean isCheckOrHolderFile(Path path) { 238 final Path fileNamePath = path.getFileName(); 239 boolean isCheckOrHolder = false; 240 if (fileNamePath != null && Files.isRegularFile(path)) { 241 final String fileName = fileNamePath.toString(); 242 isCheckOrHolder = (fileName.endsWith("Check.java") || fileName.endsWith("Holder.java")) 243 && (!fileName.startsWith("Abstract") 244 || "AbstractClassNameCheck.java".equals(fileName)); 245 } 246 return isCheckOrHolder; 247 } 248 249 /** 250 * Checks if a module is a holder type. 251 * 252 * @param moduleName the module name 253 * @return true if the module is a holder, false otherwise 254 */ 255 private static boolean isHolder(String moduleName) { 256 return moduleName.endsWith("Holder"); 257 } 258 259 /** 260 * Processes a single check class file and extracts metadata. 261 * 262 * @param path the check class file 263 * @param infos map of results 264 * @param xmlHrefMap map of XML hrefs 265 * @param packageFilter optional package to filter by, null for all 266 * @throws IllegalArgumentException if macro execution fails 267 */ 268 private static void processCheckFile(Path path, Map<String, CheckInfo> infos, 269 Map<String, String> xmlHrefMap, 270 String packageFilter) { 271 try { 272 final String moduleName = CommonUtil.getFileNameWithoutExtension(path.toString()); 273 SiteUtil.processModule(moduleName, path); 274 if (!JavadocScraperResultUtil.getModuleSinceVersion().isEmpty()) { 275 final String description = JavadocScraperResultUtil.getModuleDescription(); 276 if (!description.isEmpty()) { 277 278 final String[] moduleInfo = determineModuleInfo(path, moduleName); 279 final String packageName = moduleInfo[1]; 280 if (packageFilter == null || packageFilter.equals(packageName)) { 281 final String sanitizedDescription = sanitizeAnchorUrls(description); 282 283 final String simpleName = moduleInfo[0]; 284 final String summary = sanitizeAndFirstSentence(sanitizedDescription); 285 final String href = resolveHref(xmlHrefMap, packageName, simpleName, 286 packageFilter); 287 infos.put(simpleName, new CheckInfo(simpleName, href, summary)); 288 } 289 } 290 } 291 292 } 293 catch (MacroExecutionException exceptionThrown) { 294 throw new IllegalArgumentException(exceptionThrown); 295 } 296 } 297 298 /** 299 * Determines the simple name and package name for a check module. 300 * 301 * @param path the check class file 302 * @param moduleName the full module name 303 * @return array with [simpleName, packageName] 304 */ 305 private static String[] determineModuleInfo(Path path, String moduleName) { 306 String packageName = extractCategoryFromJavaPath(path); 307 308 if ("indentation".equals(packageName)) { 309 packageName = MISC_PACKAGE; 310 } 311 if (isHolder(moduleName)) { 312 packageName = ANNOTATION_PACKAGE; 313 } 314 final String simpleName; 315 if (isHolder(moduleName)) { 316 simpleName = moduleName; 317 } 318 else { 319 simpleName = moduleName.substring(0, moduleName.length() - "Check".length()); 320 } 321 322 return new String[] {simpleName, packageName}; 323 } 324 325 /** 326 * Builds HTML rows for both normal and holder check modules. 327 * 328 * @param infos map of collected module info 329 * @param normalRows builder for normal check rows 330 * @param holderRows builder for holder check rows 331 */ 332 private static void buildTableRows(Map<String, CheckInfo> infos, 333 StringBuilder normalRows, 334 StringBuilder holderRows) { 335 for (CheckInfo info : infos.values()) { 336 final String row = buildTableRow(info); 337 if (isHolder(info.simpleName())) { 338 holderRows.append(row); 339 } 340 else { 341 normalRows.append(row); 342 } 343 } 344 removeLeadingNewline(normalRows); 345 removeLeadingNewline(holderRows); 346 } 347 348 /** 349 * Builds a single table row for a check module. 350 * 351 * @param info check module information 352 * @return the HTML table row as a string 353 */ 354 private static String buildTableRow(CheckInfo info) { 355 final String ind10 = ModuleJavadocParsingUtil.INDENT_LEVEL_10; 356 final String ind12 = ModuleJavadocParsingUtil.INDENT_LEVEL_12; 357 final String ind14 = ModuleJavadocParsingUtil.INDENT_LEVEL_14; 358 final String ind16 = ModuleJavadocParsingUtil.INDENT_LEVEL_16; 359 360 final String cleanSummary = sanitizeAnchorUrls(info.summary()); 361 362 return ind10 + "<tr>" 363 + ind12 + TD_TAG 364 + ind14 365 + "<a href=\"" 366 + info.link() 367 + "\">" 368 + ind16 + info.simpleName() 369 + ind14 + CLOSING_ANCHOR_TAG 370 + ind12 + TD_CLOSE_TAG 371 + ind12 + TD_TAG 372 + ind14 + wrapSummary(cleanSummary) 373 + ind12 + TD_CLOSE_TAG 374 + ind10 + "</tr>"; 375 } 376 377 /** 378 * Removes leading newline characters from a StringBuilder. 379 * 380 * @param builder the StringBuilder to process 381 */ 382 private static void removeLeadingNewline(StringBuilder builder) { 383 while (!builder.isEmpty() && Character.isWhitespace(builder.charAt(0))) { 384 builder.delete(0, 1); 385 } 386 } 387 388 /** 389 * Appends the Holder Checks HTML section. 390 * 391 * @param sink the output sink 392 * @param holderRows the holder rows content 393 */ 394 private static void appendHolderSection(Sink sink, StringBuilder holderRows) { 395 final String holderSection = ModuleJavadocParsingUtil.INDENT_LEVEL_8 396 + TABLE_CLOSE_TAG 397 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 398 + DIV_CLOSE_TAG 399 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 400 + SECTION_CLOSE_TAG 401 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 402 + "<section name=\"Holder Checks\">" 403 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 404 + "<p>" 405 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 406 + "These checks aren't normal checks and are usually" 407 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 408 + "associated with a specialized filter to gather" 409 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 410 + "information the filter can't get on its own." 411 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 412 + "</p>" 413 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 414 + DIV_WRAPPER_TAG 415 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 416 + TABLE_OPEN_TAG 417 + ModuleJavadocParsingUtil.INDENT_LEVEL_10 418 + holderRows; 419 sink.rawText(holderSection); 420 } 421 422 /** 423 * Appends the filtered Holder Checks section for package views. 424 * 425 * @param sink the output sink 426 * @param holderRows the holder rows content 427 * @param packageName the package name 428 */ 429 private static void appendFilteredHolderSection(Sink sink, StringBuilder holderRows, 430 String packageName) { 431 final String packageTitle = getPackageDisplayName(packageName); 432 final String holderSection = ModuleJavadocParsingUtil.INDENT_LEVEL_8 433 + TABLE_CLOSE_TAG 434 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 435 + DIV_CLOSE_TAG 436 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 437 + SECTION_CLOSE_TAG 438 + ModuleJavadocParsingUtil.INDENT_LEVEL_4 439 + "<section name=\"" + packageTitle + " Holder Checks\">" 440 + ModuleJavadocParsingUtil.INDENT_LEVEL_6 441 + DIV_WRAPPER_TAG 442 + ModuleJavadocParsingUtil.INDENT_LEVEL_8 443 + TABLE_OPEN_TAG 444 + ModuleJavadocParsingUtil.INDENT_LEVEL_10 445 + holderRows; 446 sink.rawText(holderSection); 447 } 448 449 /** 450 * Get display name for package (capitalize first letter). 451 * 452 * @param packageName the package name 453 * @return the capitalized package name 454 */ 455 private static String getPackageDisplayName(String packageName) { 456 final String result; 457 if (packageName == null || packageName.isEmpty()) { 458 result = packageName; 459 } 460 else { 461 result = packageName.substring(0, 1).toUpperCase(Locale.ENGLISH) 462 + packageName.substring(1); 463 } 464 return result; 465 } 466 467 /** 468 * Builds map of XML file names to HTML documentation paths. 469 * 470 * @return map of lowercase check names to hrefs 471 */ 472 private static Map<String, String> buildXmlHtmlMap() { 473 final Map<String, String> map = new TreeMap<>(); 474 if (Files.exists(SITE_CHECKS_ROOT)) { 475 try { 476 final List<Path> xmlFiles = new ArrayList<>(); 477 Files.walkFileTree(SITE_CHECKS_ROOT, new SimpleFileVisitor<>() { 478 @Override 479 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 480 if (isValidXmlFile(file)) { 481 xmlFiles.add(file); 482 } 483 return FileVisitResult.CONTINUE; 484 } 485 }); 486 487 xmlFiles.forEach(path -> addXmlHtmlMapping(path, map)); 488 } 489 catch (IOException ignored) { 490 // ignore 491 } 492 } 493 return map; 494 } 495 496 /** 497 * Checks if a path is a valid XML file for processing. 498 * 499 * @param path the path to check 500 * @return true if the path is a valid XML file, false otherwise 501 */ 502 private static boolean isValidXmlFile(Path path) { 503 final Path fileName = path.getFileName(); 504 return fileName != null 505 && !("index" + XML_EXTENSION).equalsIgnoreCase(fileName.toString()) 506 && path.toString().endsWith(XML_EXTENSION) 507 && Files.isRegularFile(path); 508 } 509 510 /** 511 * Adds XML-to-HTML mapping entry to map. 512 * 513 * @param path the XML file path 514 * @param map the mapping to update 515 */ 516 private static void addXmlHtmlMapping(Path path, Map<String, String> map) { 517 final Path fileName = path.getFileName(); 518 if (fileName != null) { 519 final String fileNameString = fileName.toString(); 520 final int extensionLength = 4; 521 final String base = fileNameString.substring(0, 522 fileNameString.length() - extensionLength) 523 .toLowerCase(Locale.ROOT); 524 final Path relativePath = SITE_CHECKS_ROOT.relativize(path); 525 final String relativePathString = relativePath.toString(); 526 final String rel = relativePathString 527 .replace('\\', '/') 528 .replace(XML_EXTENSION, HTML_EXTENSION); 529 map.put(base, CHECKS + "/" + rel); 530 } 531 } 532 533 /** 534 * Resolves the href for a given check module. 535 * When packageFilter is null, returns full path: checks/category/filename.html#CheckName 536 * When packageFilter is set, returns relative path: filename.html#CheckName 537 * 538 * @param xmlMap map of XML file names to HTML paths 539 * @param category the category of the check 540 * @param simpleName simple name of the check 541 * @param packageFilter optional package filter, null for all checks 542 * @return the resolved href for the check 543 */ 544 private static String resolveHref(Map<String, String> xmlMap, String category, 545 String simpleName, @Nullable String packageFilter) { 546 final String lower = simpleName.toLowerCase(Locale.ROOT); 547 final String href = xmlMap.get(lower); 548 final String result; 549 550 if (href != null) { 551 if (packageFilter == null) { 552 result = href + ANCHOR_SEPARATOR + simpleName; 553 } 554 else { 555 final int lastSlash = href.lastIndexOf('/'); 556 final String filename; 557 if (lastSlash >= 0) { 558 filename = href.substring(lastSlash + 1); 559 } 560 else { 561 filename = href; 562 } 563 result = filename + ANCHOR_SEPARATOR + simpleName; 564 } 565 } 566 else { 567 if (packageFilter == null) { 568 result = String.format(Locale.ROOT, "%s/%s/%s.html%s%s", 569 CHECKS, category, lower, ANCHOR_SEPARATOR, simpleName); 570 } 571 else { 572 result = String.format(Locale.ROOT, "%s.html%s%s", 573 lower, ANCHOR_SEPARATOR, simpleName); 574 } 575 } 576 return result; 577 } 578 579 /** 580 * Extracts category path from a Java file path. 581 * 582 * @param javaPath the Java source file path 583 * @return the category path extracted from the Java path 584 */ 585 private static String extractCategoryFromJavaPath(Path javaPath) { 586 final Path rel = JAVA_CHECKS_ROOT.relativize(javaPath); 587 final Path parent = rel.getParent(); 588 final String result; 589 if (parent == null) { 590 result = MISC_PACKAGE; 591 } 592 else { 593 result = parent.toString().replace('\\', '/'); 594 } 595 return result; 596 } 597 598 /** 599 * Sanitizes URLs within anchor tags by removing whitespace from href attributes. 600 * 601 * @param html the HTML string containing anchor tags 602 * @return the HTML with sanitized URLs 603 */ 604 private static String sanitizeAnchorUrls(String html) { 605 final String result; 606 if (html == null || html.isEmpty()) { 607 result = html; 608 } 609 else { 610 final Matcher matcher = HREF_PATTERN.matcher(html); 611 final StringBuilder buffer = new StringBuilder(html.length()); 612 613 while (matcher.find()) { 614 final String originalUrl = matcher.group(1); 615 final String cleanedUrl = SPACE_PATTERN.matcher(originalUrl).replaceAll(""); 616 final String replacement = "href=\"" 617 + Matcher.quoteReplacement(cleanedUrl) + "\""; 618 matcher.appendReplacement(buffer, replacement); 619 } 620 matcher.appendTail(buffer); 621 622 result = buffer.toString(); 623 } 624 return result; 625 } 626 627 /** 628 * Sanitizes HTML and extracts first sentence. 629 * Preserves anchor tags while removing other HTML formatting. 630 * Also cleans whitespace from URLs in href attributes. 631 * 632 * @param html the HTML string to process 633 * @return the sanitized first sentence 634 */ 635 private static String sanitizeAndFirstSentence(String html) { 636 final String result; 637 if (html == null || html.isEmpty()) { 638 result = ""; 639 } 640 else { 641 String cleaned = sanitizeAnchorUrls(html); 642 cleaned = TAG_PATTERN.matcher(cleaned).replaceAll(""); 643 cleaned = SPACE_PATTERN.matcher(cleaned).replaceAll(" ").trim(); 644 cleaned = AMP_PATTERN.matcher(cleaned).replaceAll("&"); 645 cleaned = CODE_SPACE_PATTERN.matcher(cleaned).replaceAll(FIRST_CAPTURE_GROUP); 646 result = extractFirstSentence(cleaned); 647 } 648 return result; 649 } 650 651 /** 652 * Extracts first sentence from plain text. 653 * 654 * @param text the text to process 655 * @return the first sentence extracted from the text 656 */ 657 private static String extractFirstSentence(String text) { 658 String result = ""; 659 if (text != null && !text.isEmpty()) { 660 int end = -1; 661 for (int index = 0; index < text.length(); index++) { 662 if (text.charAt(index) == '.' 663 && (index == text.length() - 1 664 || Character.isWhitespace(text.charAt(index + 1)) 665 || text.charAt(index + 1) == '<')) { 666 end = index; 667 break; 668 } 669 } 670 if (end == -1) { 671 result = text.trim(); 672 } 673 else { 674 result = text.substring(0, end + 1).trim(); 675 } 676 } 677 return result; 678 } 679 680 /** 681 * Wraps long summaries to avoid exceeding line width. 682 * Preserves URLs in anchor tags by breaking after the opening tag's closing bracket. 683 * 684 * @param text the text to wrap 685 * @return the wrapped text 686 */ 687 private static String wrapSummary(String text) { 688 String wrapped = ""; 689 690 if (text != null && !text.isEmpty()) { 691 final String sanitized = sanitizeAnchorUrls(text); 692 693 final String indent = ModuleJavadocParsingUtil.INDENT_LEVEL_14; 694 final String clean = sanitized.trim(); 695 696 final StringBuilder result = new StringBuilder(CAPACITY); 697 int cleanIndex = 0; 698 final int cleanLen = clean.length(); 699 700 while (cleanIndex < cleanLen) { 701 final int remainingChars = cleanLen - cleanIndex; 702 703 if (remainingChars <= MAX_CONTENT_WIDTH) { 704 result.append(indent) 705 .append(clean.substring(cleanIndex)) 706 .append('\n'); 707 break; 708 } 709 710 final int idealBreak = cleanIndex + MAX_CONTENT_WIDTH; 711 final int actualBreak = calculateBreakPoint(clean, cleanIndex, idealBreak); 712 713 result.append(indent) 714 .append(clean, cleanIndex, actualBreak); 715 716 cleanIndex = actualBreak; 717 while (cleanIndex < cleanLen && clean.charAt(cleanIndex) == ' ') { 718 cleanIndex++; 719 } 720 } 721 722 wrapped = result.toString().trim(); 723 } 724 725 return wrapped; 726 } 727 728 /** 729 * Calculates the appropriate break point for text wrapping. 730 * Handles anchor tags specially to avoid breaking URLs. 731 * 732 * @param clean the cleaned text to process 733 * @param cleanIndex current position in text 734 * @param idealBreak ideal break position 735 * @return the actual break position 736 */ 737 private static int calculateBreakPoint(String clean, int cleanIndex, int idealBreak) { 738 final int anchorStart = clean.indexOf("<a ", cleanIndex); 739 final int anchorOpenEnd; 740 if (anchorStart == -1) { 741 anchorOpenEnd = -1; 742 } 743 else { 744 anchorOpenEnd = clean.indexOf('>', anchorStart); 745 } 746 747 final int actualBreak; 748 if (shouldBreakAfterAnchorOpen(anchorStart, anchorOpenEnd, idealBreak)) { 749 actualBreak = anchorOpenEnd + 1; 750 } 751 else if (shouldBreakAfterAnchorContent(anchorStart, anchorOpenEnd, 752 idealBreak, clean)) { 753 actualBreak = anchorOpenEnd + 1; 754 } 755 else { 756 actualBreak = findSafeBreakPoint(clean, cleanIndex, idealBreak); 757 } 758 759 return actualBreak; 760 } 761 762 /** 763 * Determines if break should occur after anchor opening tag. 764 * 765 * @param anchorStart start position of anchor tag 766 * @param anchorOpenEnd end position of anchor opening tag 767 * @param idealBreak ideal break position 768 * @return true if should break after anchor opening 769 */ 770 private static boolean shouldBreakAfterAnchorOpen(int anchorStart, int anchorOpenEnd, 771 int idealBreak) { 772 return anchorStart != -1 && anchorStart < idealBreak 773 && anchorOpenEnd != -1 && anchorOpenEnd >= idealBreak; 774 } 775 776 /** 777 * Determines if break should occur after anchor content. 778 * 779 * @param anchorStart start position of anchor tag 780 * @param anchorOpenEnd end position of anchor opening tag 781 * @param idealBreak ideal break position 782 * @param clean the text being processed 783 * @return true if should break after anchor content 784 */ 785 private static boolean shouldBreakAfterAnchorContent(int anchorStart, int anchorOpenEnd, 786 int idealBreak, String clean) { 787 final boolean result; 788 if (anchorStart != -1 && anchorStart < idealBreak 789 && anchorOpenEnd != -1 && anchorOpenEnd < idealBreak) { 790 final int anchorCloseStart = clean.indexOf(CLOSING_ANCHOR_TAG, anchorOpenEnd); 791 result = anchorCloseStart != -1 && anchorCloseStart >= idealBreak; 792 } 793 else { 794 result = false; 795 } 796 return result; 797 } 798 799 /** 800 * Finds a safe break point at a space character. 801 * 802 * @param text the text to search 803 * @param start the start index 804 * @param idealBreak the ideal break position 805 * @return the actual break position 806 */ 807 private static int findSafeBreakPoint(String text, int start, int idealBreak) { 808 final int actualBreak; 809 final int lastSpace = text.lastIndexOf(' ', idealBreak); 810 811 if (lastSpace > start && lastSpace >= start + MAX_CONTENT_WIDTH / 2) { 812 actualBreak = lastSpace; 813 } 814 else { 815 actualBreak = idealBreak; 816 } 817 818 return actualBreak; 819 } 820 821 /** 822 * Data holder for each Check module entry. 823 * 824 * @param simpleName check simple name 825 * @param link documentation link 826 * @param summary module summary 827 */ 828 private record CheckInfo(String simpleName, String link, String summary) { 829 } 830 831}