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    /**
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("&amp;");
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}