ChecksXmlMacro.java

///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2025 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
///////////////////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.site;

import java.nio.file.Path;
import java.util.regex.Pattern;

import org.apache.maven.doxia.macro.AbstractMacro;
import org.apache.maven.doxia.macro.Macro;
import org.apache.maven.doxia.macro.MacroExecutionException;
import org.apache.maven.doxia.macro.MacroRequest;
import org.apache.maven.doxia.sink.Sink;
import org.codehaus.plexus.component.annotations.Component;

import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;

/**
 * A macro that inserts the first sentence (summary) of a Check module's Javadoc,
 * cleaned of HTML tags and links for safe inclusion in xdoc XML.
 *
 * <p>This class is used during site generation to dynamically populate
 * {@code checks.xml} content using module-level documentation.</p>
 */
@Component(role = Macro.class, hint = "checks")
public class ChecksXmlMacro extends AbstractMacro {

    /** Pattern to remove structural HTML tags like div, p, span, em, strong. */
    private static final Pattern STRUCTURAL_TAG_PATTERN =
            Pattern.compile("(?is)</?(?:div|p|span|em|strong)[^>]*>");

    /** Pattern to remove HTML anchor tags (<a>...</a>). */
    private static final Pattern ANCHOR_TAG_PATTERN =
            Pattern.compile("(?is)<a[^>]*?>|</a>");

    /** Pattern for collapsing multiple whitespace characters into one. */
    private static final Pattern SPACE_PATTERN = Pattern.compile("\\s+");

    @Override
    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
        final Object param = request.getParameter("modulePath");
        if (param == null) {
            throw new MacroExecutionException("Parameter 'modulePath' is required.");
        }

        final Path modulePath = Path.of((String) param);
        final String moduleName = CommonUtil.getFileNameWithoutExtension(modulePath.toString());

        final DetailNode moduleJavadoc = SiteUtil.getModuleJavadoc(moduleName, modulePath);
        if (moduleJavadoc == null) {
            throw new MacroExecutionException(
                    "Javadoc of module " + moduleName + " not found.");
        }

        final String moduleDescription =
                ModuleJavadocParsingUtil.getModuleDescription(moduleJavadoc);
        final String cleanDescription = sanitize(moduleDescription);
        final String summarySentence = extractFirstSentence(cleanDescription);

        final String textToWrap = summarySentence.trim();
        final String formatted = wrapText(textToWrap, 70);
        sink.rawText(formatted);
    }

    /**
     * Extracts the first sentence (until the first period followed by whitespace or end).
     *
     * @param description the full module description text
     * @return first sentence of description
     */
    private static String extractFirstSentence(String description) {
        String result = "";
        if (description != null) {
            int endIndex = -1;
            final int descriptionLength = description.length();
            for (int index = 0; index < descriptionLength; index++) {
                if (description.charAt(index) == '.'
                        && (index == descriptionLength - 1
                        || Character.isWhitespace(description.charAt(index + 1))
                        || description.charAt(index + 1) == '<')) {
                    endIndex = index;
                    break;
                }
            }
            if (endIndex == -1) {
                result = description;
            }
            else {
                result = description.substring(0, endIndex + 1);
            }
        }
        return result.trim();
    }

    /**
     * Wraps text at approximately {@code wrapLimit} characters without breaking words.
     * Ensures total line length (including indentation and XML tags) ��� 100 chars.
     *
     * @param text the text to wrap
     * @param wrapLimit maximum number of characters per line before wrapping
     * @return wrapped text
     */
    private static String wrapText(String text, int wrapLimit) {
        final String result;
        if (text == null || text.isEmpty()) {
            result = "";
        }
        else {
            final StringBuilder wrapped = new StringBuilder(text.length() + 32);
            String remaining = text.trim();
            boolean isFirstLine = true;

            final String continuationIndent = ModuleJavadocParsingUtil.INDENT_LEVEL_14;

            int remainingLength = remaining.length();
            while (remainingLength > wrapLimit) {
                int breakIndex = remaining.lastIndexOf(' ', wrapLimit);
                if (breakIndex <= 0 || breakIndex > remainingLength) {
                    breakIndex = Math.min(wrapLimit, remainingLength);
                }

                final int safeBreakIndex = Math.min(Math.max(0, breakIndex), remainingLength);

                if (!isFirstLine) {
                    wrapped.append(continuationIndent);
                }
                wrapped.append(remaining, 0, safeBreakIndex);

                remaining = remaining.substring(safeBreakIndex).trim();
                remainingLength = remaining.length();
                isFirstLine = false;
            }

            if (!isFirstLine) {
                wrapped.append(continuationIndent);
            }

            wrapped.append(remaining);
            result = wrapped.toString();
        }
        return result;
    }

    /**
     * Cleans up unwanted HTML tags, leaving readable text only.
     * Preserves inline formatting tags like {@code <code>}.
     *
     * @param html the HTML text to clean
     * @return sanitized text without unwanted tags
     */
    private static String sanitize(String html) {
        final String result;
        if (html == null || html.isEmpty()) {
            result = "";
        }
        else {
            String cleaned = ANCHOR_TAG_PATTERN.matcher(html).replaceAll("");
            cleaned = STRUCTURAL_TAG_PATTERN.matcher(cleaned).replaceAll("");
            cleaned = SPACE_PATTERN.matcher(cleaned).replaceAll(" ");
            result = cleaned.trim();
        }
        return result;
    }
}