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}