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                final DetailNode paragraphNode = JavadocUtil.findFirstToken(
118                    node, JavadocTokenTypes.PARAGRAPH);
119                if (paragraphNode != null && JavadocMetadataScraper.isChildNodeTextMatches(
120                    paragraphNode, NOTES_LINE)) {
121
122                    notesStartIndex = node.getIndex() - 1;
123                    break;
124                }
125            }
126        }
127
128        return notesStartIndex;
129    }
130
131    /**
132     * Gets the end index of the Notes.
133     *
134     * @param moduleJavadoc javadoc of module.
135     * @param propertyNamesSet Set with property names.
136     * @return the end index.
137     */
138    private static int getNotesEndIndex(DetailNode moduleJavadoc,
139                                              Set<String> propertyNamesSet) {
140        int notesEndIndex = -1;
141
142        if (propertyNamesSet.isEmpty()) {
143            notesEndIndex += getParentSectionStartIndex(moduleJavadoc);
144        }
145        else {
146            final String somePropertyName = propertyNamesSet.iterator().next();
147
148            final Optional<DetailNode> somePropertyModuleNode =
149                SiteUtil.getPropertyJavadocNodeInModule(
150                    somePropertyName, moduleJavadoc);
151
152            if (somePropertyModuleNode.isPresent()) {
153                notesEndIndex += JavadocMetadataScraper
154                    .getParentIndexOf(somePropertyModuleNode.get());
155            }
156        }
157
158        return notesEndIndex;
159    }
160
161    /**
162     * Gets the start index of the parent subsection in module's JavaDoc.
163     *
164     * @param moduleJavadoc javadoc of module.
165     * @return start index of parent subsection.
166     */
167    private static int getParentSectionStartIndex(DetailNode moduleJavadoc) {
168        int parentStartIndex = 0;
169
170        for (DetailNode node : moduleJavadoc.getChildren()) {
171            if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
172                final DetailNode paragraphNode = JavadocUtil.findFirstToken(
173                    node, JavadocTokenTypes.PARAGRAPH);
174                if (paragraphNode != null && JavadocMetadataScraper.isParentText(paragraphNode)) {
175                    parentStartIndex = node.getIndex();
176                    break;
177                }
178            }
179        }
180
181        return parentStartIndex;
182    }
183
184    /**
185     * Writes the notes into xdoc.
186     *
187     * @param notes notes of the module.
188     * @param sink sink of the macro.
189     */
190    private static void writeOutNotes(String notes, Sink sink) {
191        final String[] moduleNotesLinesSplit = notes.split(NEWLINE);
192
193        sink.rawText(moduleNotesLinesSplit[0]);
194        String lastHtmlTag = moduleNotesLinesSplit[0];
195
196        for (int index = 1; index < moduleNotesLinesSplit.length; index++) {
197            final String currentLine = moduleNotesLinesSplit[index].trim();
198            final String processedLine;
199
200            if (currentLine.isEmpty()) {
201                processedLine = NEWLINE;
202            }
203            else if (currentLine.startsWith("<")
204                && !startsWithTextFormattingHtmlTag(currentLine)) {
205
206                processedLine = INDENT_LEVEL_8 + currentLine;
207                lastHtmlTag = currentLine;
208            }
209            else if (lastHtmlTag.contains("<pre")) {
210                final String currentLineWithPreservedIndent = moduleNotesLinesSplit[index]
211                    .substring(1);
212
213                processedLine = NEWLINE + currentLineWithPreservedIndent;
214            }
215            else {
216                processedLine = INDENT_LEVEL_10 + currentLine;
217            }
218
219            sink.rawText(processedLine);
220        }
221
222    }
223
224    /**
225     * Checks if given line starts with HTML text-formatting tag.
226     *
227     * @param line line to check on.
228     * @return whether given line starts with HTML text-formatting tag.
229     */
230    private static boolean startsWithTextFormattingHtmlTag(String line) {
231        boolean result = false;
232
233        for (String tag : HTML_TEXT_FORMAT_TAGS) {
234            if (line.startsWith(tag)) {
235                result = true;
236                break;
237            }
238        }
239
240        return result;
241    }
242
243}