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