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.nio.file.Path;
023import java.nio.file.Paths;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027import java.util.regex.Pattern;
028
029import org.apache.maven.doxia.macro.AbstractMacro;
030import org.apache.maven.doxia.macro.Macro;
031import org.apache.maven.doxia.macro.MacroExecutionException;
032import org.apache.maven.doxia.macro.MacroRequest;
033import org.apache.maven.doxia.sink.Sink;
034import org.codehaus.plexus.component.annotations.Component;
035
036import com.puppycrawl.tools.checkstyle.api.DetailNode;
037import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
038import com.puppycrawl.tools.checkstyle.meta.JavadocMetadataScraper;
039import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
040import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
041
042/**
043 * A macro that inserts a notes subsection of module from its Javadoc.
044 */
045@Component(role = Macro.class, hint = "notes")
046public class NotesMacro extends AbstractMacro {
047
048    /** "Notes:" javadoc marking. */
049    public static final String NOTES = "Notes:";
050    /** "Notes:" line with new line accounted. */
051    public static final Pattern NOTES_LINE_WITH_NEWLINE = Pattern.compile("\r?\n\\s?" + NOTES);
052    /** "Notes:" line. */
053    public static final Pattern NOTES_LINE = Pattern.compile("\\s*" + NOTES + "$");
054    /** New line escape character. */
055    private static final String NEWLINE = "\n";
056    /** A newline with 8 spaces of indentation. */
057    private static final String INDENT_LEVEL_8 = SiteUtil.getNewlineAndIndentSpaces(8);
058    /** A newline with 10 spaces of indentation. */
059    private static final String INDENT_LEVEL_10 = SiteUtil.getNewlineAndIndentSpaces(10);
060    /** A set of all html tags that need to be considered as text formatting for this macro. */
061    private static final Set<String> HTML_TEXT_FORMAT_TAGS = Set.of("<code>", "<a", "</a>", "<b>",
062        "</b>", "<strong>", "</strong>", "<i>", "</i>", "<em>", "</em>", "<small>", "</small>",
063        "<ins>", "<sub>", "<sup>");
064
065    @Override
066    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
067        final Path modulePath = Paths.get((String) request.getParameter("modulePath"));
068        final String moduleName = CommonUtil.getFileNameWithoutExtension(modulePath.toString());
069
070        final Set<String> propertyNames = getPropertyNames(moduleName);
071        final Map<String, DetailNode> moduleAndPropertiesJavadocs = SiteUtil.getPropertiesJavadocs(
072            propertyNames, moduleName, modulePath);
073
074        final DetailNode moduleJavadoc = moduleAndPropertiesJavadocs.get(moduleName);
075        if (moduleJavadoc == null) {
076            throw new MacroExecutionException("Javadoc of module " + moduleName + " is not found.");
077        }
078
079        final int notesStartIndex = getNotesStartIndex(moduleJavadoc);
080        final int notesEndIndex = getNotesEndIndex(moduleJavadoc, propertyNames);
081
082        final String unprocessedModuleNotes = JavadocMetadataScraper.constructSubTreeText(
083            moduleJavadoc, notesStartIndex, notesEndIndex);
084        final String moduleNotes = NOTES_LINE_WITH_NEWLINE.matcher(unprocessedModuleNotes)
085            .replaceAll("");
086
087        writeOutNotes(moduleNotes, sink);
088
089    }
090
091    /**
092     * Gets properties of the specified module.
093     *
094     * @param moduleName name of module.
095     * @return set of properties name if present, otherwise null.
096     * @throws MacroExecutionException if the module could not be retrieved.
097     */
098    private static Set<String> getPropertyNames(String moduleName)
099            throws MacroExecutionException {
100        final Object instance = SiteUtil.getModuleInstance(moduleName);
101        final Class<?> clss = instance.getClass();
102
103        return SiteUtil.getPropertiesForDocumentation(clss, instance);
104    }
105
106    /**
107     * Gets the start index of the Notes section.
108     *
109     * @param moduleJavadoc javadoc of module.
110     * @return start index.
111     */
112    public static int getNotesStartIndex(DetailNode moduleJavadoc) {
113        int notesStartIndex = -1;
114
115        for (DetailNode node : moduleJavadoc.getChildren()) {
116            if (node.getType() == JavadocTokenTypes.HTML_ELEMENT
117                && hasHtmlTagToStoreNotesSection(node)) {
118
119                notesStartIndex += node.getIndex();
120                break;
121            }
122        }
123
124        return notesStartIndex;
125    }
126
127    /**
128     * Checks whether html element has tag that stores notes section data.
129     *
130     * @param htmlElement html element to check.
131     * @return true if html element has tag storing notes section, otherwise false.
132     */
133    private static boolean hasHtmlTagToStoreNotesSection(DetailNode htmlElement) {
134        final DetailNode paragraphNode = JavadocUtil.findFirstToken(
135            htmlElement, JavadocTokenTypes.PARAGRAPH);
136        final Optional<DetailNode> liNode = getLiTagNode(htmlElement);
137
138        return paragraphNode != null && JavadocMetadataScraper.isChildNodeTextMatches(
139            paragraphNode, NOTES_LINE)
140            || liNode.isPresent() && JavadocMetadataScraper.isChildNodeTextMatches(
141                liNode.get(), NOTES_LINE);
142    }
143
144    /**
145     * Gets the node of Li HTML tag.
146     *
147     * @param htmlElement html element to get li tag from.
148     * @return Optional of li tag node.
149     */
150    private static Optional<DetailNode> getLiTagNode(DetailNode htmlElement) {
151        return Optional.of(htmlElement)
152            .map(element -> JavadocUtil.findFirstToken(element, JavadocTokenTypes.HTML_TAG))
153            .map(element -> JavadocUtil.findFirstToken(element, JavadocTokenTypes.HTML_ELEMENT))
154            .map(element -> JavadocUtil.findFirstToken(element, JavadocTokenTypes.LI));
155    }
156
157    /**
158     * Gets the end index of the Notes.
159     *
160     * @param moduleJavadoc javadoc of module.
161     * @param propertyNamesSet Set with property names.
162     * @return the end index.
163     */
164    private static int getNotesEndIndex(DetailNode moduleJavadoc,
165                                              Set<String> propertyNamesSet) {
166        int notesEndIndex = -1;
167
168        if (propertyNamesSet.isEmpty()) {
169            notesEndIndex += getParentSectionStartIndex(moduleJavadoc);
170        }
171        else {
172            final String somePropertyName = propertyNamesSet.iterator().next();
173
174            final Optional<DetailNode> somePropertyModuleNode =
175                SiteUtil.getPropertyJavadocNodeInModule(
176                    somePropertyName, moduleJavadoc);
177
178            if (somePropertyModuleNode.isPresent()) {
179                notesEndIndex += JavadocMetadataScraper
180                    .getParentIndexOf(somePropertyModuleNode.get());
181            }
182        }
183
184        return notesEndIndex;
185    }
186
187    /**
188     * Gets the start index of the parent subsection in module's JavaDoc.
189     *
190     * @param moduleJavadoc javadoc of module.
191     * @return start index of parent subsection.
192     */
193    private static int getParentSectionStartIndex(DetailNode moduleJavadoc) {
194        int parentStartIndex = 0;
195
196        for (DetailNode node : moduleJavadoc.getChildren()) {
197            if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
198                final DetailNode paragraphNode = JavadocUtil.findFirstToken(
199                    node, JavadocTokenTypes.PARAGRAPH);
200                if (paragraphNode != null && JavadocMetadataScraper.isParentText(paragraphNode)) {
201                    parentStartIndex = node.getIndex();
202                    break;
203                }
204            }
205        }
206
207        return parentStartIndex;
208    }
209
210    /**
211     * Writes the notes into xdoc.
212     *
213     * @param notes notes of the module.
214     * @param sink sink of the macro.
215     */
216    private static void writeOutNotes(String notes, Sink sink) {
217        final String[] moduleNotesLinesSplit = notes.split(NEWLINE);
218
219        sink.rawText(moduleNotesLinesSplit[0]);
220        String lastHtmlTag = moduleNotesLinesSplit[0];
221
222        for (int index = 1; index < moduleNotesLinesSplit.length; index++) {
223            final String currentLine = moduleNotesLinesSplit[index].trim();
224            final String processedLine;
225
226            if (currentLine.isEmpty()) {
227                processedLine = NEWLINE;
228            }
229            else if (currentLine.startsWith("<")
230                && !startsWithTextFormattingHtmlTag(currentLine)) {
231
232                processedLine = INDENT_LEVEL_8 + currentLine;
233                lastHtmlTag = currentLine;
234            }
235            else if (lastHtmlTag.contains("<pre")) {
236                final String currentLineWithPreservedIndent = moduleNotesLinesSplit[index]
237                    .substring(1);
238
239                processedLine = NEWLINE + currentLineWithPreservedIndent;
240            }
241            else {
242                processedLine = INDENT_LEVEL_10 + currentLine;
243            }
244
245            sink.rawText(processedLine);
246        }
247
248    }
249
250    /**
251     * Checks if given line starts with HTML text-formatting tag.
252     *
253     * @param line line to check on.
254     * @return whether given line starts with HTML text-formatting tag.
255     */
256    private static boolean startsWithTextFormattingHtmlTag(String line) {
257        boolean result = false;
258
259        for (String tag : HTML_TEXT_FORMAT_TAGS) {
260            if (line.startsWith(tag)) {
261                result = true;
262                break;
263            }
264        }
265
266        return result;
267    }
268
269}