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