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 * &lt;macro name="allCheckSummaries"/&gt;
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 * &lt;macro name="allCheckSummaries"&gt;
061 *   &lt;param name="package" value="annotation"/&gt;
062 * &lt;/macro&gt;
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 '&amp;' 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("&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}