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}