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    /**
069     * Matches HTML anchor tags and captures their inner text.
070     * Used to strip &lt;a&gt; elements while keeping their display text.
071     */
072    private static final Pattern LINK_PATTERN = Pattern.compile("<a[^>]*>([^<]*)</a>");
073
074    /**
075     * Matches common HTML tags such as paragraph, div, span, strong, and em.
076     * Used to remove formatting tags from the Javadoc HTML content.
077     */
078    private static final Pattern TAG_PATTERN =
079            Pattern.compile("(?i)</?(?:p|div|span|strong|em)[^>]*>");
080
081    /**
082     * Matches one or more whitespace characters.
083     * Used to normalize spacing in sanitized text.
084     */
085    private static final Pattern SPACE_PATTERN = Pattern.compile("\\s+");
086
087    /**
088     * Matches '&amp;' characters that are not part of a valid HTML entity.
089     */
090    private static final Pattern AMP_PATTERN = Pattern.compile("&(?![a-zA-Z#0-9]+;)");
091
092    /** Pattern to match trailing spaces before closing code tags. */
093    private static final Pattern CODE_SPACE_PATTERN = Pattern.compile("\\s+(</code>)");
094
095    /** Path component for source directory. */
096    private static final String SRC = "src";
097
098    /** Path component for checks directory. */
099    private static final String CHECKS = "checks";
100
101    /** Root path for Java check files. */
102    private static final Path JAVA_CHECKS_ROOT = Path.of(
103            SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle", CHECKS);
104
105    /** Root path for site check XML files. */
106    private static final Path SITE_CHECKS_ROOT = Path.of(SRC, "site", "xdoc", CHECKS);
107
108    /** Maximum line width considering indentation. */
109    private static final int MAX_LINE_WIDTH = 86;
110
111    /** XML file extension. */
112    private static final String XML_EXTENSION = ".xml";
113
114    /** HTML file extension. */
115    private static final String HTML_EXTENSION = ".html";
116
117    /** TD opening tag. */
118    private static final String TD_TAG = "<td>";
119
120    /** TD closing tag. */
121    private static final String TD_CLOSE_TAG = "</td>";
122
123    /** Package name for miscellaneous checks. */
124    private static final String MISC_PACKAGE = "misc";
125
126    /** Package name for annotation checks. */
127    private static final String ANNOTATION_PACKAGE = "annotation";
128
129    /** HTML table closing tag. */
130    private static final String TABLE_CLOSE_TAG = "</table>";
131
132    /** HTML div closing tag. */
133    private static final String DIV_CLOSE_TAG = "</div>";
134
135    /** HTML section closing tag. */
136    private static final String SECTION_CLOSE_TAG = "</section>";
137
138    /** HTML div wrapper opening tag. */
139    private static final String DIV_WRAPPER_TAG = "<div class=\"wrapper\">";
140
141    /** HTML table opening tag. */
142    private static final String TABLE_OPEN_TAG = "<table>";
143
144    /** HTML anchor separator. */
145    private static final String ANCHOR_SEPARATOR = "#";
146
147    /** Regex replacement for first capture group. */
148    private static final String FIRST_CAPTURE_GROUP = "$1";
149
150    @Override
151    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
152        final String packageFilter = (String) request.getParameter("package");
153
154        final Map<String, String> xmlHrefMap = buildXmlHtmlMap();
155        final Map<String, CheckInfo> infos = new TreeMap<>();
156
157        processCheckFiles(infos, xmlHrefMap, packageFilter);
158
159        final StringBuilder normalRows = new StringBuilder(4096);
160        final StringBuilder holderRows = new StringBuilder(512);
161
162        buildTableRows(infos, normalRows, holderRows);
163
164        sink.rawText(normalRows.toString());
165        if (packageFilter == null && !holderRows.isEmpty()) {
166            appendHolderSection(sink, holderRows);
167        }
168        else if (packageFilter != null && !holderRows.isEmpty()) {
169            appendFilteredHolderSection(sink, holderRows, packageFilter);
170        }
171    }
172
173    /**
174     * Scans Java sources and populates info map with modules having Javadoc.
175     *
176     * @param infos map of collected module info
177     * @param xmlHrefMap map of XML-to-HTML hrefs
178     * @param packageFilter optional package to filter by, null for all
179     * @throws MacroExecutionException if file walk fails
180     */
181    private static void processCheckFiles(Map<String, CheckInfo> infos,
182                                          Map<String, String> xmlHrefMap,
183                                          String packageFilter)
184            throws MacroExecutionException {
185        try {
186            final List<Path> checkFiles = new ArrayList<>();
187            Files.walkFileTree(JAVA_CHECKS_ROOT, new SimpleFileVisitor<>() {
188                @Override
189                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
190                    if (isCheckOrHolderFile(file)) {
191                        checkFiles.add(file);
192                    }
193                    return FileVisitResult.CONTINUE;
194                }
195            });
196
197            checkFiles.forEach(path -> processCheckFile(path, infos, xmlHrefMap, packageFilter));
198        }
199        catch (IOException | IllegalStateException exception) {
200            throw new MacroExecutionException("Failed to discover checks", exception);
201        }
202    }
203
204    /**
205     * Checks if a path is a Check or Holder Java file.
206     *
207     * @param path the path to check
208     * @return true if the path is a Check or Holder file, false otherwise
209     */
210    private static boolean isCheckOrHolderFile(Path path) {
211        final Path fileName = path.getFileName();
212        return fileName != null
213                && (fileName.toString().endsWith("Check.java")
214                || fileName.toString().endsWith("Holder.java"))
215                && Files.isRegularFile(path);
216    }
217
218    /**
219     * Checks if a module is a holder type.
220     *
221     * @param moduleName the module name
222     * @return true if the module is a holder, false otherwise
223     */
224    private static boolean isHolder(String moduleName) {
225        return moduleName.endsWith("Holder");
226    }
227
228    /**
229     * Processes a single check class file and extracts metadata.
230     *
231     * @param path the check class file
232     * @param infos map of results
233     * @param xmlHrefMap map of XML hrefs
234     * @param packageFilter optional package to filter by, null for all
235     * @throws IllegalArgumentException if macro execution fails
236     */
237    private static void processCheckFile(Path path, Map<String, CheckInfo> infos,
238                                         Map<String, String> xmlHrefMap,
239                                         String packageFilter) {
240        try {
241            final String moduleName = CommonUtil.getFileNameWithoutExtension(path.toString());
242            final DetailNode javadoc = SiteUtil.getModuleJavadoc(moduleName, path);
243            if (javadoc != null) {
244                final String description = getDescriptionIfPresent(javadoc);
245                if (description != null) {
246                    final boolean isHolderModule = isHolder(moduleName);
247                    final String packageName = determinePackageName(path, isHolderModule);
248
249                    if (packageFilter == null || packageFilter.equals(packageName)) {
250                        final String simpleName = determineSimpleName(moduleName, isHolderModule);
251                        final String summary = createSummary(description);
252                        final String href = resolveHref(xmlHrefMap, packageName, simpleName,
253                                packageFilter);
254                        addCheckInfo(infos, simpleName, href, summary, isHolderModule);
255                    }
256                }
257            }
258
259        }
260        catch (MacroExecutionException exceptionThrown) {
261            throw new IllegalArgumentException(exceptionThrown);
262        }
263    }
264
265    /**
266     * Determines the simple name of a check module.
267     *
268     * @param moduleName the full module name
269     * @param isHolder whether the module is a holder
270     * @return the simple name
271     */
272    private static String determineSimpleName(String moduleName, boolean isHolder) {
273        final String simpleName;
274        if (isHolder) {
275            simpleName = moduleName;
276        }
277        else {
278            simpleName = moduleName.substring(0, moduleName.length() - "Check".length());
279        }
280        return simpleName;
281    }
282
283    /**
284     * Determines the package name for a check, applying remapping rules.
285     *
286     * @param path the check class file
287     * @param isHolder whether the module is a holder
288     * @return the package name
289     */
290    private static String determinePackageName(Path path, boolean isHolder) {
291        String packageName = extractCategory(path);
292
293        // Apply remapping for indentation -> misc
294        if ("indentation".equals(packageName)) {
295            packageName = MISC_PACKAGE;
296        }
297        if (isHolder) {
298            packageName = ANNOTATION_PACKAGE;
299        }
300        return packageName;
301    }
302
303    /**
304     * Returns the module description if present and non-empty.
305     *
306     * @param javadoc the parsed Javadoc node
307     * @return the description text, or {@code null} if not present
308     */
309    @Nullable
310    private static String getDescriptionIfPresent(DetailNode javadoc) {
311        String result = null;
312        final String desc = getModuleDescriptionSafe(javadoc);
313        if (desc != null && !desc.isEmpty()) {
314            result = desc;
315        }
316        return result;
317    }
318
319    /**
320     * Produces a concise, sanitized summary from the full Javadoc description.
321     *
322     * @param description full Javadoc text
323     * @return sanitized first sentence of the description
324     */
325    private static String createSummary(String description) {
326        return sanitizeAndFirstSentence(description);
327    }
328
329    /**
330     * Extracts category name from the given Java source path.
331     *
332     * @param path source path of the class
333     * @return category name string
334     */
335    private static String extractCategory(Path path) {
336        return extractCategoryFromJavaPath(path);
337    }
338
339    /**
340     * Adds a new {@link CheckInfo} record to the provided map.
341     *
342     * @param infos map to update
343     * @param simpleName simple class name
344     * @param href documentation href
345     * @param summary short summary of the check
346     * @param isHolder true if the check is a holder module
347     */
348    private static void addCheckInfo(Map<String, CheckInfo> infos,
349                                     String simpleName,
350                                     String href,
351                                     String summary,
352                                     boolean isHolder) {
353        infos.put(simpleName, new CheckInfo(simpleName, href, summary, isHolder));
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 (info.isHolder) {
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     *
705     * @param html the HTML string to process
706     * @return the sanitized first sentence
707     */
708    private static String sanitizeAndFirstSentence(String html) {
709        final String result;
710        if (html == null || html.isEmpty()) {
711            result = "";
712        }
713        else {
714            String cleaned = LINK_PATTERN.matcher(html).replaceAll(FIRST_CAPTURE_GROUP);
715            cleaned = TAG_PATTERN.matcher(cleaned).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        final String result;
761        if (text == null || text.isEmpty()) {
762            result = "";
763        }
764        else if (text.length() <= MAX_LINE_WIDTH) {
765            result = text;
766        }
767        else {
768            result = performWrapping(text);
769        }
770        return result;
771    }
772
773    /**
774     * Performs wrapping of summary text.
775     *
776     * @param text the text to wrap
777     * @return the wrapped text
778     */
779    private static String performWrapping(String text) {
780        final int textLength = text.length();
781        final StringBuilder result = new StringBuilder(textLength + 100);
782        int pos = 0;
783        final String indent = ModuleJavadocParsingUtil.INDENT_LEVEL_14;
784        boolean firstLine = true;
785
786        while (pos < textLength) {
787            final int end = Math.min(pos + MAX_LINE_WIDTH, textLength);
788            if (end >= textLength) {
789                if (!firstLine) {
790                    result.append(indent);
791                }
792                result.append(text.substring(pos));
793                break;
794            }
795            int breakPos = text.lastIndexOf(' ', end);
796            if (breakPos <= pos) {
797                breakPos = end;
798            }
799            if (!firstLine) {
800                result.append(indent);
801            }
802            result.append(text, pos, breakPos);
803            pos = breakPos + 1;
804            firstLine = false;
805        }
806        return result.toString();
807    }
808
809    /**
810     * Data holder for each Check module entry.
811     */
812    private static final class CheckInfo {
813        /** Simple name of the check. */
814        private final String simpleName;
815        /** Documentation link. */
816        private final String link;
817        /** Short summary text. */
818        private final String summary;
819        /** Whether the module is a holder type. */
820        private final boolean isHolder;
821
822        /**
823         * Constructs an info record.
824         *
825         * @param simpleName check simple name
826         * @param link documentation link
827         * @param summary module summary
828         * @param isHolder whether holder
829         */
830        private CheckInfo(String simpleName, String link,
831                          String summary, boolean isHolder) {
832            this.simpleName = simpleName;
833            this.link = link;
834            this.summary = summary;
835            this.isHolder = isHolder;
836        }
837    }
838}