From a235d4aed7f08bb98105836476b026951eea70a1 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 30 Aug 2024 15:01:03 +0200 Subject: [PATCH 1/3] Allow multiple format and themes in the Config Doc generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The idea is to allow generating Markdown for easily publishing the config on GitHub. The Javadoc -> Markdown part will probably change a lot soon as we need to support Markdown for the IDEs. This is the fruit of a collaboration with @Athou (Jérémie Panzer), who is marked as a co-author. Co-authored-by: Jérémie Panzer --- .../config/discovery/ParsedJavadoc.java | 5 +- .../JavadocToAsciidocTransformer.java | 57 ++- .../config/model/ConfigItemCollection.java | 14 + .../config/model/JavadocElements.java | 4 +- .../scanner/JavadocConfigMappingListener.java | 2 +- .../JavadocLegacyConfigRootListener.java | 2 +- .../config/doc/GenerateAsciidocMojo.java | 407 +--------------- .../config/doc/GenerateConfigDocMojo.java | 435 ++++++++++++++++++ .../AbstractFormatter.java} | 72 +-- .../doc/generator/AsciidocFormatter.java | 42 ++ .../maven/config/doc/generator/Format.java | 35 ++ .../maven/config/doc/generator/Formatter.java | 39 ++ .../doc/generator/MarkdownFormatter.java | 48 ++ .../resources/META-INF/plexus/components.xml | 2 +- .../default}/allConfig.qute.adoc | 0 .../default}/configReference.qute.adoc | 0 .../default}/tags/configProperty.qute.adoc | 0 .../default}/tags/configSection.qute.adoc | 0 .../default}/tags/durationNote.qute.adoc | 0 .../default}/tags/envVar.qute.adoc | 0 .../default}/tags/memorySizeNote.qute.adoc | 0 .../markdown/default/allConfig.qute.md | 27 ++ .../markdown/default/configReference.qute.md | 20 + .../default/tags/configProperty.qute.md | 1 + .../default/tags/configSection.qute.md | 13 + .../default/tags/durationNote.qute.md | 17 + .../markdown/default/tags/envVar.qute.md | 1 + .../default/tags/memorySizeNote.qute.md | 8 + docs/pom.xml | 2 +- 29 files changed, 807 insertions(+), 446 deletions(-) create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java rename devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/{AsciidocFormatter.java => generator/AbstractFormatter.java} (75%) create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Format.java create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/allConfig.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/configReference.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/configProperty.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/configSection.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/durationNote.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/envVar.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/memorySizeNote.qute.adoc (100%) create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/allConfig.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/configReference.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configProperty.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configSection.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/durationNote.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/envVar.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/memorySizeNote.qute.md diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java index 287be0b9639f4..7dda7f935a508 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java @@ -1,9 +1,10 @@ package io.quarkus.annotation.processor.documentation.config.discovery; -public record ParsedJavadoc(String description, String since, String deprecated, JavadocFormat originalFormat) { +public record ParsedJavadoc(String description, String since, String deprecated, + String originalDescription, JavadocFormat originalFormat) { public static ParsedJavadoc empty() { - return new ParsedJavadoc(null, null, null, null); + return new ParsedJavadoc(null, null, null, null, null); } public boolean isEmpty() { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java index fd5a67640d0b3..2f8c05ad64ed1 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java @@ -95,14 +95,17 @@ public ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { Javadoc javadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(rawJavadoc).replaceAll("* ")); String description; + String originalDescription; JavadocFormat originalFormat; if (isAsciidoc(javadoc)) { description = handleEolInAsciidoc(javadoc); originalFormat = JavadocFormat.ASCIIDOC; + originalDescription = rawJavadoc; } else { description = htmlJavadocToAsciidoc(javadoc.getDescription()); originalFormat = JavadocFormat.JAVADOC; + originalDescription = filterRawJavadoc(javadoc.getDescription()); } Optional since = javadoc.getBlockTags().stream() @@ -121,7 +124,8 @@ public ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { description = null; } - return new ParsedJavadoc(description, since.orElse(null), deprecated.orElse(null), originalFormat); + return new ParsedJavadoc(description, since.orElse(null), deprecated.orElse(null), originalDescription, + originalFormat); } public ParsedJavadocSection parseConfigSectionJavadoc(String javadocComment) { @@ -546,4 +550,55 @@ private StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { } return sb; } + + /** + * This is not definitely not perfect but it can be used to filter the Javadoc included in Markdown. + *

+ * We will need to discuss further how to handle passing the Javadoc to the IDE. + * In Quarkus, we have Asciidoc, standard Javadoc and soon we might have Markdown javadoc. + */ + private static String filterRawJavadoc(JavadocDescription javadocDescription) { + StringBuilder sb = new StringBuilder(); + + for (JavadocDescriptionElement javadocDescriptionElement : javadocDescription.getElements()) { + if (javadocDescriptionElement instanceof JavadocInlineTag) { + JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; + String content = inlineTag.getContent().trim(); + switch (inlineTag.getType()) { + case CODE: + case VALUE: + case LITERAL: + case SYSTEM_PROPERTY: + case LINK: + case LINKPLAIN: + sb.append(""); + sb.append(escapeHtml(content)); + sb.append(""); + break; + default: + sb.append(content); + break; + } + } else { + sb.append(javadocDescriptionElement.toText()); + } + } + + return sb.toString().trim(); + } + + private static String escapeHtml(String s) { + StringBuilder out = new StringBuilder(Math.max(16, s.length())); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') { + out.append("&#"); + out.append((int) c); + out.append(';'); + } else { + out.append(c); + } + } + return out.toString(); + } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java index 01527fbbc4125..77cd8dd3f1e53 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java @@ -17,6 +17,20 @@ default List getNonDeprecatedItems() { .toList(); } + @JsonIgnore + default List getNonDeprecatedProperties() { + return getItems().stream() + .filter(i -> i instanceof ConfigProperty && !i.isDeprecated()) + .toList(); + } + + @JsonIgnore + default List getNonDeprecatedSections() { + return getItems().stream() + .filter(i -> i instanceof ConfigSection && !i.isDeprecated()) + .toList(); + } + void addItem(AbstractConfigItem item); boolean hasDurationType(); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java index 9ef41190a3268..55aa09bbeb954 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java @@ -2,11 +2,9 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnore; - public record JavadocElements(Extension extension, Map elements) { - public record JavadocElement(String description, String since, String deprecated, @JsonIgnore String rawJavadoc) { + public record JavadocElement(String description, String since, String deprecated, String rawJavadoc) { } public boolean isEmpty() { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java index 56ec7c8dc11f4..50a08674f411f 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java @@ -64,7 +64,7 @@ public void onEnclosedMethod(DiscoveryRootElement discoveryRootElement, TypeElem configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - rawJavadoc.get())); + parsedJavadoc.originalDescription())); } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java index bfbc991d0ff28..be8c164f43e60 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java @@ -64,7 +64,7 @@ public void onEnclosedField(DiscoveryRootElement discoveryRootElement, TypeEleme configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - rawJavadoc.get())); + parsedJavadoc.originalDescription())); } } } diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java index c6c6386052cc8..dee67e7ce959f 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java @@ -1,406 +1,13 @@ package io.quarkus.maven.config.doc; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.apache.maven.execution.MavenSession; -import org.apache.maven.plugin.AbstractMojo; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; - -import io.quarkus.annotation.processor.documentation.config.merger.JavadocMerger; -import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; -import io.quarkus.annotation.processor.documentation.config.merger.MergedModel; -import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; -import io.quarkus.annotation.processor.documentation.config.merger.ModelMerger; -import io.quarkus.annotation.processor.documentation.config.model.ConfigItemCollection; -import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; -import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; -import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; -import io.quarkus.annotation.processor.documentation.config.model.Extension; -import io.quarkus.qute.Engine; -import io.quarkus.qute.ReflectionValueResolver; -import io.quarkus.qute.UserTagSectionHelper; -import io.quarkus.qute.ValueResolver; +/** + * We will recommend using generate-config-doc that is more flexible. + *

+ * The GenerateConfigDocMojo defaults have to stay consistent with this. + */ @Mojo(name = "generate-asciidoc", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) -public class GenerateAsciidocMojo extends AbstractMojo { - - private static final String TARGET = "target"; - - private static final String ADOC_SUFFIX = ".adoc"; - private static final String CONFIG_ROOT_FILE_FORMAT = "%s_%s.adoc"; - private static final String EXTENSION_FILE_FORMAT = "%s.adoc"; - private static final String ALL_CONFIG_FILE_NAME = "quarkus-all-config.adoc"; - - @Parameter(defaultValue = "${session}", readonly = true) - private MavenSession mavenSession; - - @Parameter - private File scanDirectory; - - @Parameter(defaultValue = "${project.build.directory}/quarkus-generated-doc/config", required = true) - private File targetDirectory; - - @Parameter(defaultValue = "false") - private boolean generateAllConfig; - - @Parameter(defaultValue = "false") - private boolean enableEnumTooltips; - - @Parameter(defaultValue = "false") - private boolean skip; - - @Override - public void execute() throws MojoExecutionException, MojoFailureException { - if (skip) { - return; - } - - // I was unable to find an easy way to get the root directory of the project - Path resolvedScanDirectory = scanDirectory != null ? scanDirectory.toPath() - : mavenSession.getCurrentProject().getBasedir().toPath().getParent(); - Path resolvedTargetDirectory = targetDirectory.toPath(); - initTargetDirectory(resolvedTargetDirectory); - - List targetDirectories = findTargetDirectories(resolvedScanDirectory); - - JavadocRepository javadocRepository = JavadocMerger.mergeJavadocElements(targetDirectories); - MergedModel mergedModel = ModelMerger.mergeModel(javadocRepository, targetDirectories); - - AsciidocFormatter asciidocFormatter = new AsciidocFormatter(javadocRepository, enableEnumTooltips); - Engine quteEngine = initializeQuteEngine(asciidocFormatter); - - // we generate a file per extension + top level prefix - for (Entry> extensionConfigRootsEntry : mergedModel.getConfigRoots() - .entrySet()) { - Extension extension = extensionConfigRootsEntry.getKey(); - - Path configRootAdocPath = null; - - for (Entry configRootEntry : extensionConfigRootsEntry.getValue().entrySet()) { - String topLevelPrefix = configRootEntry.getKey().topLevelPrefix(); - ConfigRoot configRoot = configRootEntry.getValue(); - - if (configRoot.getNonDeprecatedItems().isEmpty()) { - continue; - } - - configRootAdocPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, - extension.artifactId(), topLevelPrefix)); - String summaryTableId = asciidocFormatter - .toAnchor(extension.artifactId() + "_" + topLevelPrefix); - - try { - Files.writeString(configRootAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); - } catch (Exception e) { - throw new MojoExecutionException("Unable to render config roots for top level prefix: " + topLevelPrefix - + " in extension: " + extension, e); - } - } - - // if we have only one top level prefix, we copy the generated file to a file named after the extension - // for simplicity's sake - if (extensionConfigRootsEntry.getValue().size() == 1 && configRootAdocPath != null) { - Path extensionAdocPath = resolvedTargetDirectory.resolve(String.format(EXTENSION_FILE_FORMAT, - extension.artifactId())); - - try { - Files.copy(configRootAdocPath, extensionAdocPath, StandardCopyOption.REPLACE_EXISTING); - } catch (Exception e) { - throw new MojoExecutionException("Unable to copy extension file for: " + extension, e); - } - } - } - - // we generate the config roots that are saved in a specific file - for (Entry specificFileConfigRootEntry : mergedModel.getConfigRootsInSpecificFile().entrySet()) { - String fileName = specificFileConfigRootEntry.getKey(); - ConfigRoot configRoot = specificFileConfigRootEntry.getValue(); - - if (configRoot.getNonDeprecatedItems().isEmpty()) { - continue; - } - - Extension extension = configRoot.getExtension(); - - if (!fileName.endsWith(".adoc")) { - fileName += ".adoc"; - } - - Path configRootAdocPath = resolvedTargetDirectory.resolve(fileName); - String summaryTableId = asciidocFormatter.toAnchor(stripAdocSuffix(fileName)); - - try { - Files.writeString(configRootAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); - } catch (Exception e) { - throw new MojoExecutionException("Unable to render config roots for specific file: " + fileName - + " in extension: " + extension, e); - } - } - - // we generate files for generated sections - for (Entry> extensionConfigSectionsEntry : mergedModel.getGeneratedConfigSections() - .entrySet()) { - Extension extension = extensionConfigSectionsEntry.getKey(); - - for (ConfigSection generatedConfigSection : extensionConfigSectionsEntry.getValue()) { - if (generatedConfigSection.getNonDeprecatedItems().isEmpty()) { - continue; - } - - Path configSectionAdocPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, - extension.artifactId(), cleanSectionPath(generatedConfigSection.getPath().property()))); - String summaryTableId = asciidocFormatter - .toAnchor(extension.artifactId() + "_" + generatedConfigSection.getPath().property()); - - try { - Files.writeString(configSectionAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, generatedConfigSection, - "_" + generatedConfigSection.getPath().property(), false)); - } catch (Exception e) { - throw new MojoExecutionException( - "Unable to render config section for section: " + generatedConfigSection.getPath().property() - + " in extension: " + extension, - e); - } - } - } - - if (generateAllConfig) { - // we generate the file centralizing all the config properties - try { - Path allConfigAdocPath = resolvedTargetDirectory.resolve(ALL_CONFIG_FILE_NAME); - - Files.writeString(allConfigAdocPath, generateAllConfig(quteEngine, mergedModel.getConfigRoots())); - } catch (Exception e) { - throw new MojoExecutionException("Unable to render all config", e); - } - } - } - - private static String generateConfigReference(Engine quteEngine, String summaryTableId, Extension extension, - ConfigItemCollection configItemCollection, String additionalAnchorPrefix, boolean searchable) { - return quteEngine.getTemplate("configReference.qute.adoc") - .data("extension", extension) - .data("configItemCollection", configItemCollection) - .data("searchable", searchable) - .data("summaryTableId", summaryTableId) - .data("additionalAnchorPrefix", additionalAnchorPrefix) - .data("includeDurationNote", configItemCollection.hasDurationType()) - .data("includeMemorySizeNote", configItemCollection.hasMemorySizeType()) - .render(); - } - - private static String generateAllConfig(Engine quteEngine, - Map> configRootsByExtensions) { - return quteEngine.getTemplate("allConfig.qute.adoc") - .data("configRootsByExtensions", configRootsByExtensions) - .data("searchable", true) - .data("summaryTableId", "all-config") - .data("additionalAnchorPrefix", "") - .data("includeDurationNote", true) - .data("includeMemorySizeNote", true) - .render(); - } - - private static void initTargetDirectory(Path resolvedTargetDirectory) throws MojoExecutionException { - try { - Files.createDirectories(resolvedTargetDirectory); - } catch (IOException e) { - throw new MojoExecutionException("Unable to create directory: " + resolvedTargetDirectory, e); - } - } - - private static List findTargetDirectories(Path scanDirectory) throws MojoExecutionException { - try { - List targets = new ArrayList<>(); - - Files.walkFileTree(scanDirectory, new SimpleFileVisitor<>() { - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - if (dir.endsWith(TARGET)) { - targets.add(dir); - - // a target directory can contain target directories for test projects - // so let's make sure we ignore whatever is nested in a target - return FileVisitResult.SKIP_SUBTREE; - } - - return FileVisitResult.CONTINUE; - } - }); - - // Make sure we are deterministic - Collections.sort(targets); - - return targets; - } catch (IOException e) { - throw new MojoExecutionException("Unable to collect the target directories", e); - } - } - - private static Engine initializeQuteEngine(AsciidocFormatter asciidocFormatter) { - Engine engine = Engine.builder() - .addDefaults() - .addSectionHelper(new UserTagSectionHelper.Factory("configProperty", "configProperty.qute.adoc")) - .addSectionHelper(new UserTagSectionHelper.Factory("configSection", "configSection.qute.adoc")) - .addSectionHelper(new UserTagSectionHelper.Factory("envVar", "envVar.qute.adoc")) - .addSectionHelper(new UserTagSectionHelper.Factory("durationNote", "durationNote.qute.adoc")) - .addSectionHelper(new UserTagSectionHelper.Factory("memorySizeNote", "memorySizeNote.qute.adoc")) - .addValueResolver(new ReflectionValueResolver()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(String.class) - .applyToName("escapeCellContent") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.escapeCellContent((String) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(String.class) - .applyToName("toAnchor") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.toAnchor((String) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigRootKey.class) - .applyToName("displayConfigRootDescription") - .applyToParameters(1) - .resolveSync(ctx -> asciidocFormatter - .displayConfigRootDescription((ConfigRootKey) ctx.getBase(), - (int) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigProperty.class) - .applyToName("toAnchor") - .applyToParameters(2) - .resolveSync(ctx -> asciidocFormatter - .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) - .artifactId() + - // the additional suffix - ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + - "_" + ((ConfigProperty) ctx.getBase()).getPath().property())) - .build()) - // we need a different anchor for sections as otherwise we can have a conflict - // (typically when you have an `enabled` property with parent name just under the section level) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigSection.class) - .applyToName("toAnchor") - .applyToParameters(2) - .resolveSync(ctx -> asciidocFormatter - .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) - .artifactId() + - // the additional suffix - ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + - "_section_" + ((ConfigSection) ctx.getBase()).getPath().property())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigProperty.class) - .applyToName("formatTypeDescription") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatTypeDescription((ConfigProperty) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigProperty.class) - .applyToName("formatDescription") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatDescription((ConfigProperty) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigProperty.class) - .applyToName("formatDefaultValue") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatDefaultValue((ConfigProperty) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigSection.class) - .applyToName("formatTitle") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatSectionTitle((ConfigSection) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigSection.class) - .applyToName("adjustedLevel") - .applyToParameters(1) - .resolveSync(ctx -> asciidocFormatter - .adjustedLevel((ConfigSection) ctx.getBase(), - (boolean) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(Extension.class) - .applyToName("formatName") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatName((Extension) ctx.getBase())) - .build()) - .build(); - - engine.putTemplate("configReference.qute.adoc", - engine.parse(getTemplate("templates/configReference.qute.adoc"))); - engine.putTemplate("allConfig.qute.adoc", - engine.parse(getTemplate("templates/allConfig.qute.adoc"))); - engine.putTemplate("configProperty.qute.adoc", - engine.parse(getTemplate("templates/tags/configProperty.qute.adoc"))); - engine.putTemplate("configSection.qute.adoc", - engine.parse(getTemplate("templates/tags/configSection.qute.adoc"))); - engine.putTemplate("envVar.qute.adoc", - engine.parse(getTemplate("templates/tags/envVar.qute.adoc"))); - engine.putTemplate("durationNote.qute.adoc", - engine.parse(getTemplate("templates/tags/durationNote.qute.adoc"))); - engine.putTemplate("memorySizeNote.qute.adoc", - engine.parse(getTemplate("templates/tags/memorySizeNote.qute.adoc"))); - - return engine; - } - - private static String getTemplate(String template) { - InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(template); - if (is == null) { - throw new IllegalArgumentException("Template does not exist: " + template); - } - - try { - return new String(is.readAllBytes(), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new UncheckedIOException("Unable to read the template: " + template, e); - } finally { - try { - is.close(); - } catch (IOException e) { - throw new UncheckedIOException("Unable close InputStream for template: " + template, e); - } - } - } - - /** - * A section path can contain quotes when being inside a Map. - *

- * While not very common, we sometimes have to generate a section inside a Map - * e.g. for the XDS config of the gRPC client. - */ - private static String cleanSectionPath(String sectionPath) { - return sectionPath.replace('"', '-'); - } - - private String stripAdocSuffix(String fileName) { - return fileName.substring(0, fileName.length() - ADOC_SUFFIX.length()); - } -} +public class GenerateAsciidocMojo extends GenerateConfigDocMojo { +} \ No newline at end of file diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java new file mode 100644 index 0000000000000..0530062b329b2 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java @@ -0,0 +1,435 @@ +package io.quarkus.maven.config.doc; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import io.quarkus.annotation.processor.documentation.config.merger.JavadocMerger; +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.merger.MergedModel; +import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; +import io.quarkus.annotation.processor.documentation.config.merger.ModelMerger; +import io.quarkus.annotation.processor.documentation.config.model.ConfigItemCollection; +import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; +import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.Extension; +import io.quarkus.maven.config.doc.generator.Format; +import io.quarkus.maven.config.doc.generator.Formatter; +import io.quarkus.qute.Engine; +import io.quarkus.qute.ReflectionValueResolver; +import io.quarkus.qute.UserTagSectionHelper; +import io.quarkus.qute.ValueResolver; + +@Mojo(name = "generate-config-doc", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) +public class GenerateConfigDocMojo extends AbstractMojo { + + private static final String TARGET = "target"; + + private static final String ADOC_SUFFIX = ".adoc"; + private static final String CONFIG_ROOT_FILE_FORMAT = "%s_%s.%s"; + private static final String EXTENSION_FILE_FORMAT = "%s.%s"; + private static final String ALL_CONFIG_FILE_FORMAT = "quarkus-all-config.%s"; + + @Parameter(defaultValue = "${session}", readonly = true) + private MavenSession mavenSession; + + // if this is switched to something else at some point, GenerateAsciidocMojo will need to be adapted + @Parameter(defaultValue = "asciidoc") + private String format; + + @Parameter(defaultValue = Format.DEFAULT_THEME) + private String theme; + + @Parameter + private File scanDirectory; + + @Parameter(defaultValue = "${project.build.directory}/quarkus-generated-doc/config", required = true) + private File targetDirectory; + + @Parameter(defaultValue = "false") + private boolean generateAllConfig; + + @Parameter(defaultValue = "false") + private boolean enableEnumTooltips; + + @Parameter(defaultValue = "false") + private boolean skip; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip) { + return; + } + + // I was unable to find an easy way to get the root directory of the project + Path resolvedScanDirectory = scanDirectory != null ? scanDirectory.toPath() + : mavenSession.getCurrentProject().getBasedir().toPath().getParent(); + Path resolvedTargetDirectory = targetDirectory.toPath(); + initTargetDirectory(resolvedTargetDirectory); + + List targetDirectories = findTargetDirectories(resolvedScanDirectory); + + JavadocRepository javadocRepository = JavadocMerger.mergeJavadocElements(targetDirectories); + MergedModel mergedModel = ModelMerger.mergeModel(javadocRepository, targetDirectories); + + Format normalizedFormat = Format.normalizeFormat(format); + + String normalizedTheme = normalizedFormat.normalizeTheme(theme); + Formatter formatter = Formatter.getFormatter(javadocRepository, enableEnumTooltips, normalizedFormat); + Engine quteEngine = initializeQuteEngine(formatter, normalizedFormat, normalizedTheme); + + // we generate a file per extension + top level prefix + for (Entry> extensionConfigRootsEntry : mergedModel.getConfigRoots() + .entrySet()) { + Extension extension = extensionConfigRootsEntry.getKey(); + + Path configRootPath = null; + + for (Entry configRootEntry : extensionConfigRootsEntry.getValue().entrySet()) { + String topLevelPrefix = configRootEntry.getKey().topLevelPrefix(); + ConfigRoot configRoot = configRootEntry.getValue(); + + if (configRoot.getNonDeprecatedItems().isEmpty()) { + continue; + } + + configRootPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, + extension.artifactId(), topLevelPrefix, normalizedFormat.getExtension())); + String summaryTableId = formatter + .toAnchor(extension.artifactId() + "_" + topLevelPrefix); + + try { + Files.writeString(configRootPath, + generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); + } catch (Exception e) { + throw new MojoExecutionException("Unable to render config roots for top level prefix: " + topLevelPrefix + + " in extension: " + extension, e); + } + } + + // if we have only one top level prefix, we copy the generated file to a file named after the extension + // for simplicity's sake + if (extensionConfigRootsEntry.getValue().size() == 1 && configRootPath != null) { + Path extensionPath = resolvedTargetDirectory.resolve(String.format(EXTENSION_FILE_FORMAT, + extension.artifactId(), normalizedFormat.getExtension())); + + try { + Files.copy(configRootPath, extensionPath, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw new MojoExecutionException("Unable to copy extension file for: " + extension, e); + } + } + } + + // we generate the config roots that are saved in a specific file + for (Entry specificFileConfigRootEntry : mergedModel.getConfigRootsInSpecificFile().entrySet()) { + String annotationFileName = specificFileConfigRootEntry.getKey(); + ConfigRoot configRoot = specificFileConfigRootEntry.getValue(); + Extension extension = configRoot.getExtension(); + + if (configRoot.getNonDeprecatedItems().isEmpty()) { + continue; + } + + String normalizedFileName = stripAdocSuffix(annotationFileName); + String fileName = normalizedFileName + "." + normalizedFormat.getExtension(); + + Path configRootPath = resolvedTargetDirectory.resolve(fileName); + String summaryTableId = formatter.toAnchor(normalizedFileName); + + try { + Files.writeString(configRootPath, + generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); + } catch (Exception e) { + throw new MojoExecutionException("Unable to render config roots for specific file: " + fileName + + " in extension: " + extension, e); + } + } + + // we generate files for generated sections + for (Entry> extensionConfigSectionsEntry : mergedModel.getGeneratedConfigSections() + .entrySet()) { + Extension extension = extensionConfigSectionsEntry.getKey(); + + for (ConfigSection generatedConfigSection : extensionConfigSectionsEntry.getValue()) { + if (generatedConfigSection.getNonDeprecatedItems().isEmpty()) { + continue; + } + + Path configSectionPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, + extension.artifactId(), cleanSectionPath(generatedConfigSection.getPath().property()), + normalizedFormat.getExtension())); + String summaryTableId = formatter + .toAnchor(extension.artifactId() + "_" + generatedConfigSection.getPath().property()); + + try { + Files.writeString(configSectionPath, + generateConfigReference(quteEngine, summaryTableId, extension, generatedConfigSection, + "_" + generatedConfigSection.getPath().property(), false)); + } catch (Exception e) { + throw new MojoExecutionException( + "Unable to render config section for section: " + generatedConfigSection.getPath().property() + + " in extension: " + extension, + e); + } + } + } + + if (generateAllConfig) { + // we generate the file centralizing all the config properties + try { + Path allConfigPath = resolvedTargetDirectory.resolve(String.format(ALL_CONFIG_FILE_FORMAT, + normalizedFormat.getExtension())); + + Files.writeString(allConfigPath, generateAllConfig(quteEngine, mergedModel.getConfigRoots())); + } catch (Exception e) { + throw new MojoExecutionException("Unable to render all config", e); + } + } + } + + private static String generateConfigReference(Engine quteEngine, String summaryTableId, Extension extension, + ConfigItemCollection configItemCollection, String additionalAnchorPrefix, boolean searchable) { + return quteEngine.getTemplate("configReference") + .data("extension", extension) + .data("configItemCollection", configItemCollection) + .data("searchable", searchable) + .data("summaryTableId", summaryTableId) + .data("additionalAnchorPrefix", additionalAnchorPrefix) + .data("includeDurationNote", configItemCollection.hasDurationType()) + .data("includeMemorySizeNote", configItemCollection.hasMemorySizeType()) + .render(); + } + + private static String generateAllConfig(Engine quteEngine, + Map> configRootsByExtensions) { + return quteEngine.getTemplate("allConfig") + .data("configRootsByExtensions", configRootsByExtensions) + .data("searchable", true) + .data("summaryTableId", "all-config") + .data("additionalAnchorPrefix", "") + .data("includeDurationNote", true) + .data("includeMemorySizeNote", true) + .render(); + } + + private static void initTargetDirectory(Path resolvedTargetDirectory) throws MojoExecutionException { + try { + Files.createDirectories(resolvedTargetDirectory); + } catch (IOException e) { + throw new MojoExecutionException("Unable to create directory: " + resolvedTargetDirectory, e); + } + } + + private static List findTargetDirectories(Path scanDirectory) throws MojoExecutionException { + try { + List targets = new ArrayList<>(); + + Files.walkFileTree(scanDirectory, new SimpleFileVisitor<>() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (dir.endsWith(TARGET)) { + targets.add(dir); + + // a target directory can contain target directories for test projects + // so let's make sure we ignore whatever is nested in a target + return FileVisitResult.SKIP_SUBTREE; + } + + return FileVisitResult.CONTINUE; + } + }); + + // Make sure we are deterministic + Collections.sort(targets); + + return targets; + } catch (IOException e) { + throw new MojoExecutionException("Unable to collect the target directories", e); + } + } + + private static Engine initializeQuteEngine(Formatter formatter, Format format, String theme) { + Engine engine = Engine.builder() + .addDefaults() + .addSectionHelper(new UserTagSectionHelper.Factory("configProperty", "configProperty")) + .addSectionHelper(new UserTagSectionHelper.Factory("configSection", "configSection")) + .addSectionHelper(new UserTagSectionHelper.Factory("envVar", "envVar")) + .addSectionHelper(new UserTagSectionHelper.Factory("durationNote", "durationNote")) + .addSectionHelper(new UserTagSectionHelper.Factory("memorySizeNote", "memorySizeNote")) + .addValueResolver(new ReflectionValueResolver()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(String.class) + .applyToName("escapeCellContent") + .applyToNoParameters() + .resolveSync(ctx -> formatter.escapeCellContent((String) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(String.class) + .applyToName("toAnchor") + .applyToNoParameters() + .resolveSync(ctx -> formatter.toAnchor((String) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigRootKey.class) + .applyToName("displayConfigRootDescription") + .applyToParameters(1) + .resolveSync(ctx -> formatter + .displayConfigRootDescription((ConfigRootKey) ctx.getBase(), + (int) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("toAnchor") + .applyToParameters(2) + .resolveSync(ctx -> formatter + .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) + .artifactId() + + // the additional suffix + ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + + "_" + ((ConfigProperty) ctx.getBase()).getPath().property())) + .build()) + // we need a different anchor for sections as otherwise we can have a conflict + // (typically when you have an `enabled` property with parent name just under the section level) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigSection.class) + .applyToName("toAnchor") + .applyToParameters(2) + .resolveSync(ctx -> formatter + .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) + .artifactId() + + // the additional suffix + ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + + "_section_" + ((ConfigSection) ctx.getBase()).getPath().property())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("formatTypeDescription") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatTypeDescription((ConfigProperty) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("formatDescription") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatDescription((ConfigProperty) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("formatDefaultValue") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatDefaultValue((ConfigProperty) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigSection.class) + .applyToName("formatTitle") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatSectionTitle((ConfigSection) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigSection.class) + .applyToName("adjustedLevel") + .applyToParameters(1) + .resolveSync(ctx -> formatter + .adjustedLevel((ConfigSection) ctx.getBase(), + (boolean) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(Extension.class) + .applyToName("formatName") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatName((Extension) ctx.getBase())) + .build()) + .build(); + + engine.putTemplate("configReference", + engine.parse(getTemplate("templates", format, theme, "configReference", false))); + engine.putTemplate("allConfig", + engine.parse(getTemplate("templates", format, theme, "allConfig", false))); + engine.putTemplate("configProperty", + engine.parse(getTemplate("templates", format, theme, "configProperty", true))); + engine.putTemplate("configSection", + engine.parse(getTemplate("templates", format, theme, "configSection", true))); + engine.putTemplate("envVar", + engine.parse(getTemplate("templates", format, theme, "envVar", true))); + engine.putTemplate("durationNote", + engine.parse(getTemplate("templates", format, theme, "durationNote", true))); + engine.putTemplate("memorySizeNote", + engine.parse(getTemplate("templates", format, theme, "memorySizeNote", true))); + + return engine; + } + + private static String getTemplate(String root, Format format, String theme, String template, boolean tag) { + List candidates = new ArrayList<>(); + candidates.add( + root + "/" + format + "/" + theme + "/" + (tag ? "tags/" : "") + template + ".qute." + format.getExtension()); + if (!Format.DEFAULT_THEME.equals(theme)) { + candidates + .add(root + "/" + format + "/" + Format.DEFAULT_THEME + "/" + (tag ? "tags/" : "") + template + ".qute." + + format.getExtension()); + } + + InputStream is = null; + ; + for (String candidate : candidates) { + is = Thread.currentThread().getContextClassLoader().getResourceAsStream(candidate); + if (is != null) { + break; + } + } + + if (is == null) { + throw new IllegalArgumentException("Unable to find a template for these candidates " + candidates); + } + + try { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException("Unable to read the template: " + template, e); + } finally { + try { + is.close(); + } catch (IOException e) { + throw new UncheckedIOException("Unable to close InputStream for template: " + template, e); + } + } + } + + /** + * A section path can contain quotes when being inside a Map. + *

+ * While not very common, we sometimes have to generate a section inside a Map + * e.g. for the XDS config of the gRPC client. + */ + private static String cleanSectionPath(String sectionPath) { + return sectionPath.replace('"', '-'); + } + + private String stripAdocSuffix(String fileName) { + return fileName.substring(0, fileName.length() - ADOC_SUFFIX.length()); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java similarity index 75% rename from devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java rename to devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java index 1617471b3260b..81f37d5b13d2a 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java @@ -1,4 +1,4 @@ -package io.quarkus.maven.config.doc; +package io.quarkus.maven.config.doc.generator; import java.text.Normalizer; import java.time.Duration; @@ -13,20 +13,18 @@ import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; import io.quarkus.annotation.processor.documentation.config.util.Types; -final class AsciidocFormatter { +abstract class AbstractFormatter implements Formatter { - private static final String TOOLTIP_MACRO = "tooltip:%s[%s]"; - private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "link:#%s[icon:question-circle[title=More information about the %s format]]"; + protected final JavadocRepository javadocRepository; + protected final boolean enableEnumTooltips; - private final JavadocRepository javadocRepository; - private final boolean enableEnumTooltips; - - AsciidocFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { + AbstractFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { this.javadocRepository = javadocRepository; this.enableEnumTooltips = enableEnumTooltips; } - boolean displayConfigRootDescription(ConfigRootKey configRootKey, int mapSize) { + @Override + public boolean displayConfigRootDescription(ConfigRootKey configRootKey, int mapSize) { if (mapSize <= 1) { return false; } @@ -34,7 +32,8 @@ boolean displayConfigRootDescription(ConfigRootKey configRootKey, int mapSize) { return configRootKey.description() != null; } - String formatDescription(ConfigProperty configProperty) { + @Override + public String formatDescription(ConfigProperty configProperty) { Optional javadocElement = javadocRepository.getElement(configProperty.getSourceClass(), configProperty.getSourceName()); @@ -42,7 +41,7 @@ String formatDescription(ConfigProperty configProperty) { return null; } - String description = javadocElement.get().description(); + String description = javadoc(javadocElement.get()); if (description == null || description.isBlank()) { return null; } @@ -50,7 +49,8 @@ String formatDescription(ConfigProperty configProperty) { return description + "\n\n"; } - String formatTypeDescription(ConfigProperty configProperty) { + @Override + public String formatTypeDescription(ConfigProperty configProperty) { String typeContent = ""; if (configProperty.isEnum() && enableEnumTooltips) { @@ -62,14 +62,13 @@ String formatTypeDescription(ConfigProperty configProperty) { return "`" + e.getValue().configValue() + "`"; } - return String.format(TOOLTIP_MACRO, e.getValue().configValue(), - cleanTooltipContent(javadocElement.get().description())); + return tooltip(e.getValue().configValue(), javadoc(javadocElement.get())); }) .collect(Collectors.joining(", ")); } else { typeContent = configProperty.getTypeDescription(); if (configProperty.getJavadocSiteLink() != null) { - typeContent = String.format("link:%s[%s]", configProperty.getJavadocSiteLink(), typeContent); + typeContent = link(configProperty.getJavadocSiteLink(), typeContent); } } if (configProperty.isList()) { @@ -77,17 +76,16 @@ String formatTypeDescription(ConfigProperty configProperty) { } if (Duration.class.getName().equals(configProperty.getType())) { - typeContent += " " + String.format(MORE_INFO_ABOUT_TYPE_FORMAT, - "duration-note-anchor-{summaryTableId}", Duration.class.getSimpleName()); + typeContent += " " + moreInformationAboutType("duration-note-anchor", Duration.class.getSimpleName()); } else if (Types.MEMORY_SIZE_TYPE.equals(configProperty.getType())) { - typeContent += " " + String.format(MORE_INFO_ABOUT_TYPE_FORMAT, - "memory-size-note-anchor-{summaryTableId}", "MemorySize"); + typeContent += " " + moreInformationAboutType("memory-size-note-anchor", "MemorySize"); } return typeContent; } - String formatDefaultValue(ConfigProperty configProperty) { + @Override + public String formatDefaultValue(ConfigProperty configProperty) { String defaultValue = configProperty.getDefaultValue(); if (defaultValue == null) { @@ -105,7 +103,7 @@ String formatDefaultValue(ConfigProperty configProperty) { enumConstant.get()); if (javadocElement.isPresent()) { - return String.format(TOOLTIP_MACRO, defaultValue, cleanTooltipContent(javadocElement.get().description())); + return tooltip(defaultValue, javadocElement.get().description()); } } } @@ -113,7 +111,8 @@ String formatDefaultValue(ConfigProperty configProperty) { return "`" + defaultValue + "`"; } - int adjustedLevel(ConfigSection configSection, boolean multiRoot) { + @Override + public int adjustedLevel(ConfigSection configSection, boolean multiRoot) { if (multiRoot) { return configSection.getLevel() + 1; } @@ -121,7 +120,8 @@ int adjustedLevel(ConfigSection configSection, boolean multiRoot) { return configSection.getLevel(); } - String escapeCellContent(String value) { + @Override + public String escapeCellContent(String value) { if (value == null) { return null; } @@ -129,7 +129,8 @@ String escapeCellContent(String value) { return value.replace("|", "\\|"); } - String toAnchor(String value) { + @Override + public String toAnchor(String value) { // remove accents value = Normalizer.normalize(value, Normalizer.Form.NFKC) .replaceAll("[àáâãäåāąă]", "a") @@ -185,7 +186,8 @@ String toAnchor(String value) { return value.toLowerCase(); } - String formatSectionTitle(ConfigSection configSection) { + @Override + public String formatSectionTitle(ConfigSection configSection) { Optional javadocElement = javadocRepository.getElement(configSection.getSourceClass(), configSection.getSourceName()); @@ -203,7 +205,8 @@ String formatSectionTitle(ConfigSection configSection) { return trimFinalDot(javadoc); } - String formatName(Extension extension) { + @Override + public String formatName(Extension extension) { if (extension.name() == null) { return extension.artifactId(); } @@ -226,14 +229,11 @@ private static String trimFinalDot(String javadoc) { return javadoc.substring(0, dotIndex); } - /** - * Note that this is extremely brittle. Apparently, colons breaks the tooltips but if escaped with \, the \ appears in the - * output. - *

- * We should probably have some warnings/errors as to what is accepted in enum Javadoc. - */ - private String cleanTooltipContent(String tooltipContent) { - return tooltipContent.replace("

", "").replace("

", "").replace("\n+\n", " ").replace("\n", " ") - .replace(":", "\\:").replace("[", "\\]").replace("]", "\\]"); - } + protected abstract String javadoc(JavadocElement javadocElement); + + protected abstract String moreInformationAboutType(String anchorRoot, String type); + + protected abstract String link(String href, String description); + + protected abstract String tooltip(String value, String javadocDescription); } diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java new file mode 100644 index 0000000000000..e0d834eb1c225 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java @@ -0,0 +1,42 @@ +package io.quarkus.maven.config.doc.generator; + +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; + +final class AsciidocFormatter extends AbstractFormatter { + + private static final String TOOLTIP_MACRO = "tooltip:%s[%s]"; + private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "link:#%s[icon:question-circle[title=More information about the %s format]]"; + + AsciidocFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { + super(javadocRepository, enableEnumTooltips); + } + + protected String moreInformationAboutType(String anchorRoot, String type) { + return String.format(MORE_INFO_ABOUT_TYPE_FORMAT, anchorRoot + "-{summaryTableId}", type); + } + + protected String tooltip(String value, String javadocDescription) { + return String.format(TOOLTIP_MACRO, value, cleanTooltipContent(javadocDescription)); + } + + /** + * Note that this is extremely brittle. Apparently, colons breaks the tooltips but if escaped with \, the \ appears in the + * output. + *

+ * We should probably have some warnings/errors as to what is accepted in enum Javadoc. + */ + private String cleanTooltipContent(String tooltipContent) { + return tooltipContent.replace("

", "").replace("

", "").replace("\n+\n", " ").replace("\n", " ") + .replace(":", "\\:").replace("[", "\\]").replace("]", "\\]"); + } + + protected String link(String href, String description) { + return String.format("link:%s[%s]", href, description); + } + + @Override + protected String javadoc(JavadocElements.JavadocElement javadocElement) { + return javadocElement.description(); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Format.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Format.java new file mode 100644 index 0000000000000..905d8fa14b74e --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Format.java @@ -0,0 +1,35 @@ +package io.quarkus.maven.config.doc.generator; + +import java.util.Set; + +public enum Format { + + asciidoc("adoc", Set.of(Format.DEFAULT_THEME)), + markdown("md", Set.of(Format.DEFAULT_THEME, "github")); + + public static final String DEFAULT_THEME = "default"; + + private final String extension; + private final Set supportedThemes; + + private Format(String extension, Set supportedThemes) { + this.extension = extension; + this.supportedThemes = supportedThemes; + } + + public String getExtension() { + return extension; + } + + public String normalizeTheme(String theme) { + theme = theme.trim(); + + return supportedThemes.contains(theme) ? theme : DEFAULT_THEME; + } + + public static Format normalizeFormat(String format) { + format = format.trim(); + + return Format.valueOf(format); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java new file mode 100644 index 0000000000000..3552ab26f6e20 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java @@ -0,0 +1,39 @@ +package io.quarkus.maven.config.doc.generator; + +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; +import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.Extension; + +public interface Formatter { + + boolean displayConfigRootDescription(ConfigRootKey configRootKey, int mapSize); + + String formatDescription(ConfigProperty configProperty); + + String formatTypeDescription(ConfigProperty configProperty); + + String formatDefaultValue(ConfigProperty configProperty); + + int adjustedLevel(ConfigSection configSection, boolean multiRoot); + + String escapeCellContent(String value); + + String toAnchor(String value); + + String formatSectionTitle(ConfigSection configSection); + + String formatName(Extension extension); + + static Formatter getFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips, Format format) { + switch (format) { + case asciidoc: + return new AsciidocFormatter(javadocRepository, enableEnumTooltips); + case markdown: + return new MarkdownFormatter(javadocRepository, enableEnumTooltips); + default: + throw new IllegalArgumentException("Unsupported format: " + format); + } + } +} \ No newline at end of file diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java new file mode 100644 index 0000000000000..eaf13fda73cf7 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java @@ -0,0 +1,48 @@ +package io.quarkus.maven.config.doc.generator; + +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; + +final class MarkdownFormatter extends AbstractFormatter { + + private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "[🛈](#%s)"; + + MarkdownFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { + super(javadocRepository, enableEnumTooltips); + } + + @Override + public String formatSectionTitle(ConfigSection configSection) { + // markdown only has 6 heading levels + int headingLevel = Math.min(6, 2 + configSection.getLevel()); + return "#".repeat(headingLevel) + " " + super.formatSectionTitle(configSection); + } + + @Override + public String escapeCellContent(String value) { + String cellContent = super.escapeCellContent(value); + return cellContent == null ? null : cellContent.replace("\n\n", "

").replace("\n", " "); + } + + @Override + protected String moreInformationAboutType(String anchorRoot, String type) { + return MORE_INFO_ABOUT_TYPE_FORMAT.formatted(anchorRoot); + } + + @Override + protected String link(String href, String description) { + return String.format("[%2$s](%1$s)", href, description); + } + + @Override + protected String tooltip(String value, String javadocDescription) { + // we don't have tooltip support in Markdown + return "`" + value + "`"; + } + + @Override + protected String javadoc(JavadocElements.JavadocElement javadocElement) { + return javadocElement.rawJavadoc(); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/META-INF/plexus/components.xml b/devtools/config-doc-maven-plugin/src/main/resources/META-INF/plexus/components.xml index 615f650ef1fa4..4d890ae972d01 100644 --- a/devtools/config-doc-maven-plugin/src/main/resources/META-INF/plexus/components.xml +++ b/devtools/config-doc-maven-plugin/src/main/resources/META-INF/plexus/components.xml @@ -11,7 +11,7 @@ - io.quarkus:quarkus-config-doc-maven-plugin:${project.version}:generate-asciidoc + io.quarkus:quarkus-config-doc-maven-plugin:${project.version}:generate-config-doc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/allConfig.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/allConfig.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/allConfig.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/allConfig.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configProperty.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/configProperty.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configProperty.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/configProperty.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configSection.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/configSection.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configSection.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/configSection.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/durationNote.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/durationNote.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/durationNote.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/durationNote.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/envVar.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/envVar.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/envVar.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/envVar.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/memorySizeNote.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/memorySizeNote.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/memorySizeNote.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/memorySizeNote.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/allConfig.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/allConfig.qute.md new file mode 100644 index 0000000000000..5b89612f0fa23 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/allConfig.qute.md @@ -0,0 +1,27 @@ +🔒: Configuration property fixed at build time - All other configuration properties are overridable at runtime + +{#for extensionConfigRootsEntry in configRootsByExtensions} + +# {extensionConfigRootsEntry.key.formatName} + +| Configuration property | Type | Default | +|------------------------|------|---------| +{#for configRoot in extensionConfigRootsEntry.value.values} +{#for item in configRoot.items} +{#if !item.deprecated} +{#if !item.isSection} +{#configProperty configProperty=item extension=extensionConfigRootsEntry.key additionalAnchorPrefix=additionalAnchorPrefix /} +{#else} +{#configSection configSection=item extension=extensionConfigRootsEntry.key additionalAnchorPrefix=additionalAnchorPrefix /} +{/if} +{/if} +{/for} +{/for} +{/for} + +{#if includeDurationNote} +{#durationNote summaryTableId /} +{/if} +{#if includeMemorySizeNote} +{#memorySizeNote summaryTableId /} +{/if} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/configReference.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/configReference.qute.md new file mode 100644 index 0000000000000..3c5237e397e66 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/configReference.qute.md @@ -0,0 +1,20 @@ +🔒: Configuration property fixed at build time - All other configuration properties are overridable at runtime + +{#if configItemCollection.nonDeprecatedProperties} +| Configuration property | Type | Default | +|------------------------|------|---------| +{#for property in configItemCollection.nonDeprecatedProperties} +{#configProperty configProperty=property extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} +{/for} +{/if} +{#for section in configItemCollection.nonDeprecatedSections} + +{#configSection configSection=section extension=extension additionalAnchorPrefix=additionalAnchorPrefix displayConfigRootDescription=false /} +{/for} + +{#if includeDurationNote} +{#durationNote summaryTableId /} +{/if} +{#if includeMemorySizeNote} +{#memorySizeNote summaryTableId /} +{/if} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configProperty.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configProperty.qute.md new file mode 100644 index 0000000000000..ce0462b7b2ce1 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configProperty.qute.md @@ -0,0 +1 @@ +| {#if configProperty.phase.fixedAtBuildTime}🔒 {/if}`{configProperty.path.property}`{#for additionalPath in configProperty.additionalPaths}
`{additionalPath.property}`{/for}

{configProperty.formatDescription.escapeCellContent.or("")}{#envVar configProperty /} | {configProperty.formatTypeDescription.escapeCellContent.or("")} | {#if configProperty.defaultValue}{configProperty.formatDefaultValue.escapeCellContent}{/if} | diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configSection.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configSection.qute.md new file mode 100644 index 0000000000000..69f5a2012af94 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configSection.qute.md @@ -0,0 +1,13 @@ +{configSection.formatTitle} + +{#if configSection.nonDeprecatedProperties} +| Configuration property | Type | Default | +|------------------------|------|---------| +{#for property in configSection.nonDeprecatedProperties} +{#configProperty configProperty=property extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} +{/for} +{/if} +{#for subsection in configSection.nonDeprecatedSections} + +{#configSection configSection=subsection extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} +{/for} \ No newline at end of file diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/durationNote.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/durationNote.qute.md new file mode 100644 index 0000000000000..94d2ce93f735d --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/durationNote.qute.md @@ -0,0 +1,17 @@ + + +> [!NOTE] +> ### About the Duration format +> +> To write duration values, use the standard `java.time.Duration` format. +> See the [Duration#parse()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)) Java API documentation] for more information. +> +> You can also use a simplified format, starting with a number: +> +> * If the value is only a number, it represents time in seconds. +> * If the value is a number followed by `ms`, it represents time in milliseconds. +> +> In other cases, the simplified format is translated to the `java.time.Duration` format for parsing: +> +> * If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`. +> * If the value is a number followed by `d`, it is prefixed with `P`. diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/envVar.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/envVar.qute.md new file mode 100644 index 0000000000000..0037f6eee6545 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/envVar.qute.md @@ -0,0 +1 @@ +Environment variable: `{configProperty.path.environmentVariable}` \ No newline at end of file diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/memorySizeNote.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/memorySizeNote.qute.md new file mode 100644 index 0000000000000..f6ce0be9db3ff --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/memorySizeNote.qute.md @@ -0,0 +1,8 @@ + + +> [!NOTE] +> ### About the MemorySize format +> +> A size configuration option recognizes strings in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`. +> +> If no suffix is given, assume bytes. diff --git a/docs/pom.xml b/docs/pom.xml index ee82231a6665a..fe8a3b2c4e450 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -3210,7 +3210,7 @@ generate-config-doc process-resources - generate-asciidoc + generate-config-doc ${skipDocs} From ec49d73af8577c8f1510bf393759b5bbce408189 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 10 Sep 2024 17:29:54 +0200 Subject: [PATCH 2/3] Config Doc - Reinstate Reactive Oracle Client doc In the end, let's keep it around but handles it properly in the generated config doc. --- .../io/quarkus/maven/config/doc/GenerateAsciidocMojo.java | 2 +- .../io/quarkus/maven/config/doc/GenerateConfigDocMojo.java | 4 +--- .../templates/asciidoc/default/configReference.qute.adoc | 3 +++ docs/src/main/asciidoc/datasource.adoc | 2 +- docs/src/main/asciidoc/reactive-sql-clients.adoc | 2 +- .../oracle/client/runtime/DataSourceReactiveOracleConfig.java | 1 - 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java index dee67e7ce959f..a1ed3bf1bae0e 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java @@ -10,4 +10,4 @@ */ @Mojo(name = "generate-asciidoc", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) public class GenerateAsciidocMojo extends GenerateConfigDocMojo { -} \ No newline at end of file +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java index 0530062b329b2..ed5626b9f1bf1 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java @@ -111,9 +111,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { String topLevelPrefix = configRootEntry.getKey().topLevelPrefix(); ConfigRoot configRoot = configRootEntry.getValue(); - if (configRoot.getNonDeprecatedItems().isEmpty()) { - continue; - } + // here we generate a file even if there are no items as it's used for the Reactive Oracle SQL client configRootPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, extension.artifactId(), topLevelPrefix, normalizedFormat.getExtension())); diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc index c694aeaf7a289..c5151982992c2 100644 --- a/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc @@ -16,6 +16,9 @@ h|Default {#configProperty configProperty=item extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} {/if} +{#else} +3+|No configuration properties found. + {/for} |=== diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index 4828026f53e22..70cfc46a743fa 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -952,7 +952,7 @@ include::{generated-dir}/config/quarkus-reactive-mssql-client.adoc[opts=optional ==== Reactive Oracle-specific configuration -At the moment, there are no Oracle-specific configuration properties. +include::{generated-dir}/config/quarkus-reactive-oracle-client.adoc[opts=optional, leveloffset=+1] ==== Reactive PostgreSQL-specific configuration diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 4ce05554a1b89..5b64977d6a91c 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -922,7 +922,7 @@ include::{generated-dir}/config/quarkus-reactive-mssql-client.adoc[opts=optional === Oracle -At the moment, there are no Oracle-specific configuration properties. +include::{generated-dir}/config/quarkus-reactive-oracle-client.adoc[opts=optional, leveloffset=+1] === PostgreSQL diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/DataSourceReactiveOracleConfig.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/DataSourceReactiveOracleConfig.java index fd546210e454f..778e30be8ac13 100644 --- a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/DataSourceReactiveOracleConfig.java +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/DataSourceReactiveOracleConfig.java @@ -5,5 +5,4 @@ @ConfigGroup public interface DataSourceReactiveOracleConfig { - // when adding properties here, make sure you include the generated doc in datasource.adoc and reactive-sql-clients.adoc } From 3cfa644c4924cc6d2f0721fa6b16eede58949ae4 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 10 Sep 2024 19:33:04 +0200 Subject: [PATCH 3/3] Config Doc - Prepare the arrival of Markdown For IDEs, we will need to be able to convert our javadoc to Markdown. And obviously, transforming our javadoc to Asciidoc right in the generated YAML files is a bad idea. We now pass the raw Javadoc and it is going to be transformed at a later stage. --- .../config/ConfigDocExtensionProcessor.java | 7 +- .../config/discovery/JavadocFormat.java | 6 - .../config/discovery/ParsedJavadoc.java | 7 +- .../discovery/ParsedJavadocSection.java | 6 +- .../JavadocToAsciidocTransformer.java | 237 +++--------------- .../JavadocToMarkdownTransformer.java | 87 +++++++ .../config/formatter/JavadocTransformer.java | 20 ++ .../config/model/JavadocElements.java | 2 +- .../config/model/JavadocFormat.java | 8 + .../AbstractJavadocConfigListener.java | 22 +- .../scanner/JavadocConfigMappingListener.java | 39 +-- .../JavadocLegacyConfigRootListener.java | 39 +-- .../config/util/JavadocUtil.java | 158 ++++++++++++ .../processor/util/ElementUtil.java | 7 +- ...ocToAsciidocTransformerConfigItemTest.java | 199 ++++++++------- ...oAsciidocTransformerConfigSectionTest.java | 90 +------ .../config/util/JavadocUtilTest.java | 110 ++++++++ .../doc/generator/AbstractFormatter.java | 13 +- .../doc/generator/AsciidocFormatter.java | 12 +- .../doc/generator/MarkdownFormatter.java | 12 +- 20 files changed, 632 insertions(+), 449 deletions(-) delete mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/JavadocFormat.java create mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java create mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java create mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java index d0ad35940cf60..75c1e996e5ebd 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java @@ -71,7 +71,12 @@ public void finalizeProcessing() { Properties javadocProperties = new Properties(); for (Entry javadocElementEntry : configCollector.getJavadocElements().entrySet()) { - javadocProperties.put(javadocElementEntry.getKey(), javadocElementEntry.getValue().rawJavadoc()); + if (javadocElementEntry.getValue().description() == null + || javadocElementEntry.getValue().description().isBlank()) { + continue; + } + + javadocProperties.put(javadocElementEntry.getKey(), javadocElementEntry.getValue().description()); } utils.filer().write(Outputs.META_INF_QUARKUS_JAVADOC, javadocProperties); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/JavadocFormat.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/JavadocFormat.java deleted file mode 100644 index 027f96a25db24..0000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/JavadocFormat.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.quarkus.annotation.processor.documentation.config.discovery; - -public enum JavadocFormat { - ASCIIDOC, - JAVADOC; -} \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java index 7dda7f935a508..3e6860a917ebc 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java @@ -1,10 +1,11 @@ package io.quarkus.annotation.processor.documentation.config.discovery; -public record ParsedJavadoc(String description, String since, String deprecated, - String originalDescription, JavadocFormat originalFormat) { +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public record ParsedJavadoc(String description, JavadocFormat format, String since, String deprecated) { public static ParsedJavadoc empty() { - return new ParsedJavadoc(null, null, null, null, null); + return new ParsedJavadoc(null, null, null, null); } public boolean isEmpty() { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java index 5f56acacb9cb3..6d1cbbabc6cc1 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java @@ -1,8 +1,10 @@ package io.quarkus.annotation.processor.documentation.config.discovery; -public record ParsedJavadocSection(String title, String details, String deprecated) { +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public record ParsedJavadocSection(String title, String details, JavadocFormat format, String deprecated) { public static ParsedJavadocSection empty() { - return new ParsedJavadocSection(null, null, null); + return new ParsedJavadocSection(null, null, null, null); } } \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java index 2f8c05ad64ed1..5007f9ce71bc0 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java @@ -1,6 +1,5 @@ package io.quarkus.annotation.processor.documentation.config.formatter; -import java.util.Optional; import java.util.regex.Pattern; import org.jsoup.Jsoup; @@ -9,29 +8,17 @@ import com.github.javaparser.StaticJavaParser; import com.github.javaparser.javadoc.Javadoc; -import com.github.javaparser.javadoc.JavadocBlockTag; -import com.github.javaparser.javadoc.JavadocBlockTag.Type; -import com.github.javaparser.javadoc.description.JavadocDescription; import com.github.javaparser.javadoc.description.JavadocDescriptionElement; import com.github.javaparser.javadoc.description.JavadocInlineTag; -import io.quarkus.annotation.processor.documentation.config.discovery.JavadocFormat; -import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; -import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; import io.quarkus.annotation.processor.documentation.config.util.ConfigNamingUtil; public final class JavadocToAsciidocTransformer { - public static final JavadocToAsciidocTransformer INSTANCE = new JavadocToAsciidocTransformer(); - public static final JavadocToAsciidocTransformer INLINE_MACRO_INSTANCE = new JavadocToAsciidocTransformer(true); - private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); - private static final Pattern REPLACE_WINDOWS_EOL = Pattern.compile("\r\n"); - private static final Pattern REPLACE_MACOS_EOL = Pattern.compile("\r"); private static final Pattern STARTING_SPACE = Pattern.compile("^ +"); - private static final String DOT = "."; - private static final String BACKTICK = "`"; private static final String HASH = "#"; private static final String STAR = "*"; @@ -75,130 +62,31 @@ public final class JavadocToAsciidocTransformer { private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE = "[quote]\n____"; private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END = "____"; - private final boolean inlineMacroMode; - - private JavadocToAsciidocTransformer(boolean inlineMacroMode) { - this.inlineMacroMode = inlineMacroMode; - } - private JavadocToAsciidocTransformer() { - this(false); } - public ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { - if (rawJavadoc == null || rawJavadoc.isBlank()) { - return ParsedJavadoc.empty(); - } - - // the parser expects all the lines to start with "* " - // we add it as it has been previously removed - Javadoc javadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(rawJavadoc).replaceAll("* ")); - - String description; - String originalDescription; - JavadocFormat originalFormat; - - if (isAsciidoc(javadoc)) { - description = handleEolInAsciidoc(javadoc); - originalFormat = JavadocFormat.ASCIIDOC; - originalDescription = rawJavadoc; - } else { - description = htmlJavadocToAsciidoc(javadoc.getDescription()); - originalFormat = JavadocFormat.JAVADOC; - originalDescription = filterRawJavadoc(javadoc.getDescription()); - } - - Optional since = javadoc.getBlockTags().stream() - .filter(t -> t.getType() == Type.SINCE) - .map(JavadocBlockTag::getContent) - .map(JavadocDescription::toText) - .findFirst(); - - Optional deprecated = javadoc.getBlockTags().stream() - .filter(t -> t.getType() == Type.DEPRECATED) - .map(JavadocBlockTag::getContent) - .map(JavadocDescription::toText) - .findFirst(); + public static String toAsciidoc(String javadoc, JavadocFormat format) { + return toAsciidoc(javadoc, format, false); + } - if (description != null && description.isBlank()) { - description = null; + public static String toAsciidoc(String javadoc, JavadocFormat format, boolean inlineMacroMode) { + if (javadoc == null || javadoc.isBlank()) { + return null; } - return new ParsedJavadoc(description, since.orElse(null), deprecated.orElse(null), originalDescription, - originalFormat); - } - - public ParsedJavadocSection parseConfigSectionJavadoc(String javadocComment) { - if (javadocComment == null || javadocComment.trim().isEmpty()) { - return ParsedJavadocSection.empty(); + if (format == JavadocFormat.ASCIIDOC) { + return javadoc; + } else if (format == JavadocFormat.MARKDOWN) { + throw new IllegalArgumentException("Conversion from Markdown to Asciidoc is not supported"); } // the parser expects all the lines to start with "* " // we add it as it has been previously removed - javadocComment = START_OF_LINE.matcher(javadocComment).replaceAll("* "); - Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocComment); + Javadoc parsedJavadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(javadoc).replaceAll("* ")); - Optional deprecated = javadoc.getBlockTags().stream() - .filter(t -> t.getType() == Type.DEPRECATED) - .map(JavadocBlockTag::getContent) - .map(JavadocDescription::toText) - .findFirst(); - - String asciidoc; - if (isAsciidoc(javadoc)) { - asciidoc = handleEolInAsciidoc(javadoc); - } else { - asciidoc = htmlJavadocToAsciidoc(javadoc.getDescription()); - } - - if (asciidoc == null || asciidoc.isBlank()) { - return ParsedJavadocSection.empty(); - } - - final int newLineIndex = asciidoc.indexOf(NEW_LINE); - final int dotIndex = asciidoc.indexOf(DOT); - - final int endOfTitleIndex; - if (newLineIndex > 0 && newLineIndex < dotIndex) { - endOfTitleIndex = newLineIndex; - } else { - endOfTitleIndex = dotIndex; - } - - if (endOfTitleIndex == -1) { - final String title = asciidoc.replaceAll("^([^\\w])+", "").trim(); - - return new ParsedJavadocSection(title, null, deprecated.orElse(null)); - } else { - final String title = asciidoc.substring(0, endOfTitleIndex).replaceAll("^([^\\w])+", "").trim(); - final String details = asciidoc.substring(endOfTitleIndex + 1).trim(); - - return new ParsedJavadocSection(title, details.isBlank() ? null : details, deprecated.orElse(null)); - } - } - - private String handleEolInAsciidoc(Javadoc javadoc) { - // it's Asciidoc, so we just pass through - // it also uses platform specific EOL, so we need to convert them back to \n - String asciidoc = javadoc.getDescription().toText(); - asciidoc = REPLACE_WINDOWS_EOL.matcher(asciidoc).replaceAll("\n"); - asciidoc = REPLACE_MACOS_EOL.matcher(asciidoc).replaceAll("\n"); - return asciidoc; - } - - private boolean isAsciidoc(Javadoc javadoc) { - for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { - if ("asciidoclet".equals(blockTag.getTagName())) { - return true; - } - } - return false; - } - - private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { StringBuilder sb = new StringBuilder(); - for (JavadocDescriptionElement javadocDescriptionElement : javadocDescription.getElements()) { + for (JavadocDescriptionElement javadocDescriptionElement : parsedJavadoc.getDescription().getElements()) { if (javadocDescriptionElement instanceof JavadocInlineTag) { JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; String content = inlineTag.getContent().trim(); @@ -208,7 +96,7 @@ private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { case LITERAL: case SYSTEM_PROPERTY: sb.append('`'); - appendEscapedAsciiDoc(sb, content); + appendEscapedAsciiDoc(sb, content, inlineMacroMode); sb.append('`'); break; case LINK: @@ -217,7 +105,7 @@ private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { content = ConfigNamingUtil.hyphenate(content.substring(1)); } sb.append('`'); - appendEscapedAsciiDoc(sb, content); + appendEscapedAsciiDoc(sb, content, inlineMacroMode); sb.append('`'); break; default: @@ -225,20 +113,22 @@ private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { break; } } else { - appendHtml(sb, Jsoup.parseBodyFragment(javadocDescriptionElement.toText())); + appendHtml(sb, Jsoup.parseBodyFragment(javadocDescriptionElement.toText()), inlineMacroMode); } } - return trim(sb); + String asciidoc = trim(sb); + + return asciidoc.isBlank() ? null : asciidoc; } - private void appendHtml(StringBuilder sb, Node node) { + private static void appendHtml(StringBuilder sb, Node node, boolean inlineMacroMode) { for (Node childNode : node.childNodes()) { switch (childNode.nodeName()) { case PARAGRAPH_NODE: newLine(sb); newLine(sb); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); break; case PREFORMATED_NODE: newLine(sb); @@ -258,7 +148,7 @@ private void appendHtml(StringBuilder sb, Node node) { newLine(sb); sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE); newLine(sb); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); newLineIfNeeded(sb); sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END); newLine(sb); @@ -267,7 +157,7 @@ private void appendHtml(StringBuilder sb, Node node) { case ORDERED_LIST_NODE: case UN_ORDERED_LIST_NODE: newLine(sb); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); break; case LIST_ITEM_NODE: final String marker = childNode.parent().nodeName().equals(ORDERED_LIST_NODE) @@ -275,59 +165,59 @@ private void appendHtml(StringBuilder sb, Node node) { : UNORDERED_LIST_ITEM_ASCIDOC_STYLE; newLine(sb); sb.append(marker); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); break; case LINK_NODE: final String link = childNode.attr(HREF_ATTRIBUTE); sb.append("link:"); sb.append(link); final StringBuilder caption = new StringBuilder(); - appendHtml(caption, childNode); + appendHtml(caption, childNode, inlineMacroMode); sb.append(String.format(LINK_ATTRIBUTE_FORMAT, trim(caption))); break; case CODE_NODE: sb.append(BACKTICK); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(BACKTICK); break; case BOLD_NODE: case STRONG_NODE: sb.append(STAR); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(STAR); break; case EMPHASIS_NODE: case ITALICS_NODE: sb.append(UNDERSCORE); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(UNDERSCORE); break; case UNDERLINE_NODE: sb.append(UNDERLINE_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(HASH); break; case SMALL_NODE: sb.append(SMALL_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(HASH); break; case BIG_NODE: sb.append(BIG_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(HASH); break; case SUB_SCRIPT_NODE: sb.append(SUB_SCRIPT_ASCIDOC_STYLE); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(SUB_SCRIPT_ASCIDOC_STYLE); break; case SUPER_SCRIPT_NODE: sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); break; case DEL_NODE: @@ -335,7 +225,7 @@ private void appendHtml(StringBuilder sb, Node node) { case STRIKE_NODE: sb.append(LINE_THROUGH_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(HASH); break; case NEW_LINE_NODE: @@ -356,10 +246,10 @@ private void appendHtml(StringBuilder sb, Node node) { text = startingSpaceMatcher.replaceFirst(""); } - appendEscapedAsciiDoc(sb, text); + appendEscapedAsciiDoc(sb, text, inlineMacroMode); break; default: - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); break; } } @@ -435,7 +325,7 @@ private static StringBuilder trimText(StringBuilder sb, String charsToTrim) { return sb; } - private StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { + private static StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { int i = 0; /* trim leading whitespace */ LOOP: while (i < text.length()) { @@ -501,7 +391,7 @@ private StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { return sb; } - private StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { + private static StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text, boolean inlineMacroMode) { boolean escaping = false; for (int i = 0; i < text.length(); i++) { final char ch = text.charAt(i); @@ -550,55 +440,4 @@ private StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { } return sb; } - - /** - * This is not definitely not perfect but it can be used to filter the Javadoc included in Markdown. - *

- * We will need to discuss further how to handle passing the Javadoc to the IDE. - * In Quarkus, we have Asciidoc, standard Javadoc and soon we might have Markdown javadoc. - */ - private static String filterRawJavadoc(JavadocDescription javadocDescription) { - StringBuilder sb = new StringBuilder(); - - for (JavadocDescriptionElement javadocDescriptionElement : javadocDescription.getElements()) { - if (javadocDescriptionElement instanceof JavadocInlineTag) { - JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; - String content = inlineTag.getContent().trim(); - switch (inlineTag.getType()) { - case CODE: - case VALUE: - case LITERAL: - case SYSTEM_PROPERTY: - case LINK: - case LINKPLAIN: - sb.append(""); - sb.append(escapeHtml(content)); - sb.append(""); - break; - default: - sb.append(content); - break; - } - } else { - sb.append(javadocDescriptionElement.toText()); - } - } - - return sb.toString().trim(); - } - - private static String escapeHtml(String s) { - StringBuilder out = new StringBuilder(Math.max(16, s.length())); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') { - out.append("&#"); - out.append((int) c); - out.append(';'); - } else { - out.append(c); - } - } - return out.toString(); - } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java new file mode 100644 index 0000000000000..fd5796ed7b8b0 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java @@ -0,0 +1,87 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import java.util.regex.Pattern; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.description.JavadocDescription; +import com.github.javaparser.javadoc.description.JavadocDescriptionElement; +import com.github.javaparser.javadoc.description.JavadocInlineTag; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public class JavadocToMarkdownTransformer { + + private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); + + public static String toMarkdown(String javadoc, JavadocFormat format) { + if (javadoc == null || javadoc.isBlank()) { + return null; + } + + if (format == JavadocFormat.MARKDOWN) { + return javadoc; + } else if (format == JavadocFormat.JAVADOC) { + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + Javadoc parsedJavadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(javadoc).replaceAll("* ")); + + // HTML is valid Javadoc but we need to drop the Javadoc tags e.g. {@link ...} + return simplifyJavadoc(parsedJavadoc.getDescription()); + } + + // it's Asciidoc, the fun begins... + return ""; + } + + /** + * This is not definitely not perfect but it can be used to filter the Javadoc included in Markdown. + *

+ * We will need to discuss further how to handle passing the Javadoc to the IDE. + * In Quarkus, we have Asciidoc, standard Javadoc and soon we might have Markdown javadoc. + */ + private static String simplifyJavadoc(JavadocDescription javadocDescription) { + StringBuilder sb = new StringBuilder(); + + for (JavadocDescriptionElement javadocDescriptionElement : javadocDescription.getElements()) { + if (javadocDescriptionElement instanceof JavadocInlineTag) { + JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; + String content = inlineTag.getContent().trim(); + switch (inlineTag.getType()) { + case CODE: + case VALUE: + case LITERAL: + case SYSTEM_PROPERTY: + case LINK: + case LINKPLAIN: + sb.append(""); + sb.append(escapeHtml(content)); + sb.append(""); + break; + default: + sb.append(content); + break; + } + } else { + sb.append(javadocDescriptionElement.toText()); + } + } + + return sb.toString().trim(); + } + + private static String escapeHtml(String s) { + StringBuilder out = new StringBuilder(Math.max(16, s.length())); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') { + out.append("&#"); + out.append((int) c); + out.append(';'); + } else { + out.append(c); + } + } + return out.toString(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java new file mode 100644 index 0000000000000..59da647afdfa3 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java @@ -0,0 +1,20 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public final class JavadocTransformer { + + private JavadocTransformer() { + } + + public static String transform(String javadoc, JavadocFormat fromFormat, JavadocFormat toFormat) { + switch (toFormat) { + case ASCIIDOC: + return JavadocToAsciidocTransformer.toAsciidoc(javadoc, fromFormat); + case MARKDOWN: + return JavadocToMarkdownTransformer.toMarkdown(javadoc, fromFormat); + default: + throw new IllegalArgumentException("Converting to " + toFormat + " is not supported"); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java index 55aa09bbeb954..8a3db353d4c9f 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java @@ -4,7 +4,7 @@ public record JavadocElements(Extension extension, Map elements) { - public record JavadocElement(String description, String since, String deprecated, String rawJavadoc) { + public record JavadocElement(String description, JavadocFormat format, String since, String deprecated) { } public boolean isEmpty() { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java new file mode 100644 index 0000000000000..b44020aa4ebb1 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java @@ -0,0 +1,8 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +public enum JavadocFormat { + + ASCIIDOC, + MARKDOWN, + JAVADOC; +} \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java index 490866047cf7d..a2fe0e875223a 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java @@ -9,8 +9,8 @@ import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; -import io.quarkus.annotation.processor.documentation.config.formatter.JavadocToAsciidocTransformer; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; import io.quarkus.annotation.processor.documentation.config.util.Markers; import io.quarkus.annotation.processor.util.Config; import io.quarkus.annotation.processor.util.Utils; @@ -41,13 +41,15 @@ public Optional onConfigRoot(TypeElement configRoot) { return Optional.empty(); } - ParsedJavadocSection parsedJavadocSection = JavadocToAsciidocTransformer.INSTANCE - .parseConfigSectionJavadoc(rawJavadoc.get()); + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc.get()); + if (parsedJavadocSection.title() == null) { + return Optional.empty(); + } configCollector.addJavadocElement( configRoot.getQualifiedName().toString(), - new JavadocElement(parsedJavadocSection.title(), null, parsedJavadocSection.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); return Optional.empty(); } @@ -69,13 +71,17 @@ public void onResolvedEnum(TypeElement enumTypeElement) { continue; } - ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc.get()); + + if (parsedJavadoc.description() == null) { + continue; + } configCollector.addJavadocElement( enumTypeElement.getQualifiedName().toString() + Markers.DOT + enumElement.getSimpleName() .toString(), - new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java index 50a08674f411f..3a978fd690ea9 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java @@ -1,7 +1,5 @@ package io.quarkus.annotation.processor.documentation.config.scanner; -import java.util.Optional; - import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; @@ -9,8 +7,8 @@ import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; -import io.quarkus.annotation.processor.documentation.config.formatter.JavadocToAsciidocTransformer; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; import io.quarkus.annotation.processor.documentation.config.util.Markers; import io.quarkus.annotation.processor.documentation.config.util.Types; import io.quarkus.annotation.processor.util.Config; @@ -38,33 +36,36 @@ public void onEnclosedMethod(DiscoveryRootElement discoveryRootElement, TypeElem return; } - Optional rawJavadoc = utils.element().getJavadoc(method); + String rawJavadoc = utils.element().getJavadoc(method).orElse(""); boolean isSection = utils.element().isAnnotationPresent(method, Types.ANNOTATION_CONFIG_DOC_SECTION); - if (rawJavadoc.isEmpty()) { - // We require a Javadoc for config items that are not config groups except if they are a section - if (!resolvedType.isConfigGroup() || isSection) { - utils.element().addMissingJavadocError(method); - } - return; - } - if (isSection) { // for sections, we only keep the title - ParsedJavadocSection parsedJavadocSection = JavadocToAsciidocTransformer.INSTANCE - .parseConfigSectionJavadoc(rawJavadoc.get()); + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc); + + if (parsedJavadocSection.title() == null) { + return; + } configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), - new JavadocElement(parsedJavadocSection.title(), null, parsedJavadocSection.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); } else { - ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc); + + // We require a Javadoc for config items that are not config groups except if they are a section + if (parsedJavadoc.description() == null) { + if (parsedJavadoc.deprecated() == null && !resolvedType.isConfigGroup()) { + utils.element().addMissingJavadocError(method); + } + return; + } configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), - new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - parsedJavadoc.originalDescription())); + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java index be8c164f43e60..638a7bfb4ed4b 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java @@ -1,7 +1,5 @@ package io.quarkus.annotation.processor.documentation.config.scanner; -import java.util.Optional; - import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -9,8 +7,8 @@ import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; -import io.quarkus.annotation.processor.documentation.config.formatter.JavadocToAsciidocTransformer; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; import io.quarkus.annotation.processor.documentation.config.util.Markers; import io.quarkus.annotation.processor.documentation.config.util.Types; import io.quarkus.annotation.processor.util.Config; @@ -38,33 +36,36 @@ public void onEnclosedField(DiscoveryRootElement discoveryRootElement, TypeEleme return; } - Optional rawJavadoc = utils.element().getJavadoc(field); + String rawJavadoc = utils.element().getJavadoc(field).orElse(""); boolean isSection = utils.element().isAnnotationPresent(field, Types.ANNOTATION_CONFIG_DOC_SECTION); - if (rawJavadoc.isEmpty()) { - // We require a Javadoc for config items that are not config groups except if they are a section - if (!resolvedType.isConfigGroup() || isSection) { - utils.element().addMissingJavadocError(field); - } - return; - } - if (isSection) { // for sections, we only keep the title - ParsedJavadocSection parsedJavadocSection = JavadocToAsciidocTransformer.INSTANCE - .parseConfigSectionJavadoc(rawJavadoc.get()); + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc); + + if (parsedJavadocSection.title() == null) { + return; + } configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), - new JavadocElement(parsedJavadocSection.title(), null, parsedJavadocSection.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); } else { - ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc); + + // We require a Javadoc for config items that are not config groups except if they are a section + if (parsedJavadoc.description() == null) { + if (parsedJavadoc.deprecated() == null && !resolvedType.isConfigGroup()) { + utils.element().addMissingJavadocError(field); + } + return; + } configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), - new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - parsedJavadoc.originalDescription())); + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java index 8ca9e307b0ca0..2bed8914e4cb0 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java @@ -2,12 +2,31 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jsoup.Jsoup; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.JavadocBlockTag; +import com.github.javaparser.javadoc.JavadocBlockTag.Type; +import com.github.javaparser.javadoc.description.JavadocDescription; + +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + public final class JavadocUtil { + private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); + private static final Pattern REPLACE_WINDOWS_EOL = Pattern.compile("\r\n"); + private static final Pattern REPLACE_MACOS_EOL = Pattern.compile("\r"); + private static final String DOT = "."; + private static final String NEW_LINE = "\n"; + static final String VERTX_JAVA_DOC_SITE = "https://vertx.io/docs/apidocs/"; static final String OFFICIAL_JAVA_DOC_BASE_LINK = "https://docs.oracle.com/en/java/javase/17/docs/api/java.base/"; static final String AGROAL_API_JAVA_DOC_SITE = "https://javadoc.io/doc/io.agroal/agroal-api/latest/"; @@ -25,6 +44,118 @@ public final class JavadocUtil { private JavadocUtil() { } + public static ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { + if (rawJavadoc == null || rawJavadoc.isBlank()) { + return ParsedJavadoc.empty(); + } + + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + Javadoc javadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(rawJavadoc).replaceAll("* ")); + + String description; + JavadocFormat format; + + if (isAsciidoc(javadoc)) { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.ASCIIDOC; + } else if (isMarkdown(javadoc)) { + // this is to prepare the Markdown Javadoc that will come up soon enough + // I don't know exactly how the parser will deal with them though + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.MARKDOWN; + } else { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.JAVADOC; + } + + Optional since = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.SINCE) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + Optional deprecated = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.DEPRECATED) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + if (description != null && description.isBlank()) { + description = null; + } + + return new ParsedJavadoc(description, format, since.orElse(null), deprecated.orElse(null)); + } + + public static ParsedJavadocSection parseConfigSectionJavadoc(String javadocComment) { + if (javadocComment == null || javadocComment.trim().isEmpty()) { + return ParsedJavadocSection.empty(); + } + + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + javadocComment = START_OF_LINE.matcher(javadocComment).replaceAll("* "); + Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocComment); + + Optional deprecated = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.DEPRECATED) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + String description; + JavadocFormat format; + + if (isAsciidoc(javadoc)) { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.ASCIIDOC; + } else if (isMarkdown(javadoc)) { + // this is to prepare the Markdown Javadoc that will come up soon enough + // I don't know exactly how the parser will deal with them though + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.MARKDOWN; + } else { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.JAVADOC; + } + + if (description == null || description.isBlank()) { + return ParsedJavadocSection.empty(); + } + + final int newLineIndex = description.indexOf(NEW_LINE); + final int dotIndex = description.indexOf(DOT); + + final int endOfTitleIndex; + if (newLineIndex > 0 && newLineIndex < dotIndex) { + endOfTitleIndex = newLineIndex; + } else { + endOfTitleIndex = dotIndex; + } + + String title; + String details; + + if (endOfTitleIndex == -1) { + title = description.trim(); + details = null; + } else { + title = description.substring(0, endOfTitleIndex).trim(); + details = description.substring(endOfTitleIndex + 1).trim(); + } + + if (title.contains("<")) { + title = Jsoup.parse(title).text(); + } + + title = title.replaceAll("^([^\\w])+", ""); + + return new ParsedJavadocSection(title == null || title.isBlank() ? null : title, + details == null || details.isBlank() ? null : details, format, + deprecated.orElse(null)); + } + /** * Get javadoc link of a given type value */ @@ -59,6 +190,33 @@ public static String getJavadocSiteLink(String binaryName) { return null; } + private static String normalizeEol(String javadoc) { + // it's Asciidoc, so we just pass through + // it also uses platform specific EOL, so we need to convert them back to \n + String normalizedJavadoc = javadoc; + normalizedJavadoc = REPLACE_WINDOWS_EOL.matcher(normalizedJavadoc).replaceAll("\n"); + normalizedJavadoc = REPLACE_MACOS_EOL.matcher(normalizedJavadoc).replaceAll("\n"); + return normalizedJavadoc; + } + + private static boolean isAsciidoc(Javadoc javadoc) { + for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { + if ("asciidoclet".equals(blockTag.getTagName())) { + return true; + } + } + return false; + } + + private static boolean isMarkdown(Javadoc javadoc) { + for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { + if ("markdown".equals(blockTag.getTagName())) { + return true; + } + } + return false; + } + private static String getJavaDocLinkForType(String type) { int beginOfWrappedTypeIndex = type.indexOf("<"); if (beginOfWrappedTypeIndex != -1) { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java index eaf41d1ec52a5..86d07a9229ef1 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java @@ -172,9 +172,10 @@ public Optional getJavadoc(Element e) { } public void addMissingJavadocError(Element e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e, e); + String error = "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e; + + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error, e); + throw new IllegalStateException(error); } public boolean isJdkClass(TypeElement e) { diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java index 3bab12d00c215..546e8cc05b11f 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java @@ -11,51 +11,42 @@ import org.junit.jupiter.params.provider.ValueSource; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; public class JavadocToAsciidocTransformerConfigItemTest { - @Test - public void parseNullJavaDoc() { - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(null); - assertNull(parsed.description()); - } - @Test public void removeParagraphIndentation() { - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE - .parseConfigItemJavadoc("First paragraph

Second Paragraph"); - assertEquals("First paragraph +\n +\nSecond Paragraph", parsed.description()); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("First paragraph

Second Paragraph"); + assertEquals("First paragraph +\n +\nSecond Paragraph", + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); } @Test public void parseUntrimmedJavaDoc() { - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(" "); - assertNull(parsed.description()); - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("

"); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(" "); assertNull(parsed.description()); - } - - @Test - public void parseSimpleJavaDoc() { - String javaDoc = "hello world"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(javaDoc, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc("

"); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertNull(description); } @Test public void parseJavaDocWithParagraph() { String javaDoc = "hello

world

"; String expectedOutput = "hello\n\nworld"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); javaDoc = "hello world

bonjour

le monde

"; expectedOutput = "hello world\n\nbonjour\n\nle monde"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test @@ -63,55 +54,64 @@ public void parseJavaDocWithStyles() { // Bold String javaDoc = "hello world"; String expectedOutput = "hello *world*"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); javaDoc = "hello world"; expectedOutput = "hello *world*"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // Emphasized javaDoc = "hello world"; expectedOutput = "_hello world_"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // Italics javaDoc = "hello world"; expectedOutput = "_hello world_"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // Underline javaDoc = "hello world"; expectedOutput = "[.underline]#hello world#"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // small javaDoc = "quarkus subatomic"; expectedOutput = "[.small]#quarkus subatomic#"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // big javaDoc = "hello world"; expectedOutput = "[.big]#hello world#"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // line through javaDoc = "hello monolith world"; expectedOutput = "[.line-through]#hello #[.line-through]#monolith #[.line-through]#world#"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // superscript and subscript javaDoc = "cloud in-premise"; expectedOutput = "^cloud ^~in-premise~"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); } @Test @@ -123,9 +123,10 @@ public void parseJavaDocWithLiTagsInsideUlTag() { "" + ""; String expectedOutput = "List:\n\n - 1\n - 2"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test @@ -137,76 +138,93 @@ public void parseJavaDocWithLiTagsInsideOlTag() { "" + ""; String expectedOutput = "List:\n\n . 1\n . 2"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithLinkInlineSnippet() { String javaDoc = "{@link firstlink} {@link #secondlink} \n {@linkplain #third.link}"; String expectedOutput = "`firstlink` `secondlink` `third.link`"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithLinkTag() { String javaDoc = "this is a hello link"; String expectedOutput = "this is a link:http://link.com[hello] link"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithCodeInlineSnippet() { String javaDoc = "{@code true} {@code false}"; String expectedOutput = "`true` `false`"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithLiteralInlineSnippet() { String javaDoc = "{@literal java.util.Boolean}"; String expectedOutput = "`java.util.Boolean`"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithValueInlineSnippet() { String javaDoc = "{@value 10s}"; String expectedOutput = "`10s`"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithUnknownInlineSnippet() { String javaDoc = "{@see java.util.Boolean}"; String expectedOutput = "java.util.Boolean"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithUnknownNode() { String javaDoc = "hello"; String expectedOutput = "hello"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithBlockquoteBlock() { + ParsedJavadoc parsed = JavadocUtil + .parseConfigItemJavadoc("See Section 4.5.5 of the JSR 380 specification, specifically\n" + + "\n" + + "
\n" + + "In sub types (be it sub classes/interfaces or interface implementations), no parameter constraints may\n" + + "be declared on overridden or implemented methods, nor may parameters be marked for cascaded validation.\n" + + "This would pose a strengthening of preconditions to be fulfilled by the caller.\n" + + "
\nThat was interesting, wasn't it?"); + assertEquals("See Section 4.5.5 of the JSR 380 specification, specifically\n" + "\n" + "[quote]\n" @@ -215,49 +233,37 @@ public void parseJavaDocWithBlockquoteBlock() { + "____\n" + "\n" + "That was interesting, wasn't it?", - JavadocToAsciidocTransformer.INSTANCE - .parseConfigItemJavadoc("See Section 4.5.5 of the JSR 380 specification, specifically\n" - + "\n" - + "
\n" - + "In sub types (be it sub classes/interfaces or interface implementations), no parameter constraints may\n" - + "be declared on overridden or implemented methods, nor may parameters be marked for cascaded validation.\n" - + "This would pose a strengthening of preconditions to be fulfilled by the caller.\n" - + "
\nThat was interesting, wasn't it?") - .description()); + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + + parsed = JavadocUtil.parseConfigItemJavadoc( + "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz"); assertEquals( "Some HTML entities & special characters:\n\n```\n|[/variant]|/[/variant]\n```\n\nbaz", - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc( - "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz") - .description()); + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); // TODO // assertEquals("Example:\n\n```\nfoo\nbar\n```", - // JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); + // JavadocUtil.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); } @Test public void parseJavaDocWithCodeBlock() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("Example:\n\n
\nfoo\nbar\n
\n\nbaz"); + assertEquals("Example:\n\n```\nfoo\nbar\n```\n\nbaz", - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("Example:\n\n
\nfoo\nbar\n
\n\nbaz") - .description()); + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + + parsed = JavadocUtil.parseConfigItemJavadoc( + "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz"); assertEquals( "Some HTML entities & special characters:\n\n```\n|[/variant]|/[/variant]\n```\n\nbaz", - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc( - "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz") - .description()); + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); // TODO // assertEquals("Example:\n\n```\nfoo\nbar\n```", - // JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); - } - - @Test - public void since() { - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("Javadoc text\n\n@since 1.2.3"); - assertEquals("Javadoc text", parsed.description()); - assertEquals("1.2.3", parsed.since()); + // JavadocUtil.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); } @Test @@ -276,8 +282,9 @@ public void asciidoc() { "And some code\n" + "----"; - assertEquals(asciidoc, - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet").description()); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet"); + + assertEquals(asciidoc, JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); } @Test @@ -289,8 +296,9 @@ public void asciidocLists() { " * 1.2\n" + "* 2"; - assertEquals(asciidoc, - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet").description()); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet"); + + assertEquals(asciidoc, JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); } @ParameterizedTest @@ -299,7 +307,8 @@ public void escape(String ch) { final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; - final String asciiDoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc).description(); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); final String expected = "
\n

Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + ", JavaDoc tag " + ch + " " + ch + ch + "

\n
"; @@ -312,8 +321,8 @@ public void escapeInsideInlineElement(String ch) { final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; - final String asciiDoc = JavadocToAsciidocTransformer.INLINE_MACRO_INSTANCE.parseConfigItemJavadoc(javaDoc) - .description(); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format(), true); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); if (ch.equals("]")) { @@ -329,7 +338,8 @@ public void escapePlus() { final String javaDoc = "Inline + ++, HTML tag glob + ++, {@code JavaDoc tag + ++}"; final String expected = "
\n

Inline + ++, HTML tag glob + ++, JavaDoc tag + ++

\n
"; - final String asciiDoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc).description(); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); assertEquals(expected, actual); } @@ -342,7 +352,8 @@ public void escapeBrackets(String ch) { final String expected = "
\n

Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + "

\n
"; - final String asciiDoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc).description(); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); assertEquals(expected, actual); } diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java index 190b32c9bba84..9ab4290efa591 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java @@ -5,25 +5,19 @@ import org.junit.jupiter.api.Test; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; public class JavadocToAsciidocTransformerConfigSectionTest { - @Test - public void parseNullSection() { - ParsedJavadocSection parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(null); - assertEquals(null, parsed.details()); - assertEquals(null, parsed.title()); - } - @Test public void parseUntrimmedJavaDoc() { - ParsedJavadocSection parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(" "); - assertEquals(null, parsed.title()); - assertEquals(null, parsed.details()); + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc(" "); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.title(), parsed.format())); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.details(), parsed.format())); - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc("

"); - assertEquals(null, parsed.title()); - assertEquals(null, parsed.details()); + parsed = JavadocUtil.parseConfigSectionJavadoc("

"); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.title(), parsed.format())); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.details(), parsed.format())); } @Test @@ -43,10 +37,9 @@ public void passThroughAConfigSectionInAsciiDoc() { String asciidoc = "=== " + title + "\n\n" + details; - ParsedJavadocSection sectionHolder = JavadocToAsciidocTransformer.INSTANCE - .parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); - assertEquals(title, sectionHolder.title()); - assertEquals(details, sectionHolder.details()); + ParsedJavadocSection sectionHolder = JavadocUtil.parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); + assertEquals(title, JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.title(), sectionHolder.format())); + assertEquals(details, JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.details(), sectionHolder.format())); asciidoc = "Asciidoc title. \n" + "\n" + @@ -62,66 +55,7 @@ public void passThroughAConfigSectionInAsciiDoc() { "And some code\n" + "----"; - sectionHolder = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); - assertEquals("Asciidoc title", sectionHolder.title()); - } - - @Test - public void parseSectionWithoutIntroduction() { - /** - * Simple javadoc - */ - String javaDoc = "Config Section"; - String expectedTitle = "Config Section"; - String expectedDetails = null; - ParsedJavadocSection sectionHolder = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc); - assertEquals(expectedDetails, sectionHolder.details()); - assertEquals(expectedTitle, sectionHolder.title()); - - javaDoc = "Config Section."; - expectedTitle = "Config Section"; - expectedDetails = null; - sectionHolder = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc); - assertEquals(expectedDetails, sectionHolder.details()); - assertEquals(expectedTitle, sectionHolder.title()); - - /** - * html javadoc - */ - javaDoc = "

Config Section

"; - expectedTitle = "Config Section"; - expectedDetails = null; - sectionHolder = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc); - assertEquals(expectedDetails, sectionHolder.details()); - assertEquals(expectedTitle, sectionHolder.title()); - } - - @Test - public void parseSectionWithIntroduction() { - /** - * Simple javadoc - */ - String javaDoc = "Config Section .Introduction"; - String expectedDetails = "Introduction"; - String expectedTitle = "Config Section"; - assertEquals(expectedTitle, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).title()); - assertEquals(expectedDetails, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).details()); - - /** - * html javadoc - */ - javaDoc = "

Config Section

. Introduction"; - expectedDetails = "Introduction"; - assertEquals(expectedDetails, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).details()); - assertEquals(expectedTitle, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).title()); - } - - @Test - public void properlyParseConfigSectionWrittenInHtml() { - String javaDoc = "

Config Section.

This is section introduction"; - String expectedDetails = "This is section introduction"; - String title = "Config Section"; - assertEquals(expectedDetails, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).details()); - assertEquals(title, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).title()); + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); + assertEquals("Asciidoc title", JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.title(), sectionHolder.format())); } } diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java index 9fab0e55e0e52..1b16410465b84 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java @@ -11,8 +11,118 @@ import org.junit.jupiter.api.Test; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; + public class JavadocUtilTest { + @Test + public void parseNullJavaDoc() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(null); + assertNull(parsed.description()); + } + + @Test + public void parseSimpleJavaDoc() { + String javaDoc = "hello world"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + + assertEquals(javaDoc, parsed.description()); + } + + @Test + public void since() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("Javadoc text\n\n@since 1.2.3"); + assertEquals("Javadoc text", parsed.description()); + assertEquals("1.2.3", parsed.since()); + } + + @Test + public void deprecated() { + ParsedJavadoc parsed = JavadocUtil + .parseConfigItemJavadoc("@deprecated JNI is always enabled starting from GraalVM 19.3.1."); + assertEquals(null, parsed.description()); + } + + @Test + public void parseNullSection() { + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc(null); + assertEquals(null, parsed.details()); + assertEquals(null, parsed.title()); + } + + @Test + public void parseSimpleSection() { + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc("title"); + assertEquals("title", parsed.title()); + assertEquals(null, parsed.details()); + } + + @Test + public void parseSectionWithIntroduction() { + /** + * Simple javadoc + */ + String javaDoc = "Config Section .Introduction"; + String expectedDetails = "Introduction"; + String expectedTitle = "Config Section"; + assertEquals(expectedTitle, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + + /** + * html javadoc + */ + javaDoc = "

Config Section

. Introduction"; + expectedDetails = "Introduction"; + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + assertEquals(expectedTitle, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void parseSectionWithParagraph() { + String javaDoc = "Dev Services\n

\nDev Services allows Quarkus to automatically start Elasticsearch in dev and test mode."; + assertEquals("Dev Services", JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void properlyParseConfigSectionWrittenInHtml() { + String javaDoc = "Config Section.

This is section introduction"; + String expectedDetails = "

This is section introduction"; + String title = "Config Section"; + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + assertEquals(title, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void parseSectionWithoutIntroduction() { + /** + * Simple javadoc + */ + String javaDoc = "Config Section"; + String expectedTitle = "Config Section"; + String expectedDetails = null; + ParsedJavadocSection sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + + javaDoc = "Config Section."; + expectedTitle = "Config Section"; + expectedDetails = null; + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + + /** + * html javadoc + */ + javaDoc = "

Config Section

"; + expectedTitle = "Config Section"; + expectedDetails = null; + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + } + @Test public void shouldReturnEmptyListForPrimitiveValue() { String value = JavadocUtil.getJavadocSiteLink("int"); diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java index 81f37d5b13d2a..49384bb924e52 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java @@ -5,12 +5,14 @@ import java.util.Optional; import java.util.stream.Collectors; +import io.quarkus.annotation.processor.documentation.config.formatter.JavadocTransformer; import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; import io.quarkus.annotation.processor.documentation.config.model.Extension; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; import io.quarkus.annotation.processor.documentation.config.util.Types; abstract class AbstractFormatter implements Formatter { @@ -41,7 +43,8 @@ public String formatDescription(ConfigProperty configProperty) { return null; } - String description = javadoc(javadocElement.get()); + String description = JavadocTransformer.transform(javadocElement.get().description(), javadocElement.get().format(), + javadocFormat()); if (description == null || description.isBlank()) { return null; } @@ -62,7 +65,8 @@ public String formatTypeDescription(ConfigProperty configProperty) { return "`" + e.getValue().configValue() + "`"; } - return tooltip(e.getValue().configValue(), javadoc(javadocElement.get())); + return tooltip(e.getValue().configValue(), JavadocTransformer + .transform(javadocElement.get().description(), javadocElement.get().format(), javadocFormat())); }) .collect(Collectors.joining(", ")); } else { @@ -196,7 +200,8 @@ public String formatSectionTitle(ConfigSection configSection) { "Couldn't find section title for: " + configSection.getSourceClass() + "#" + configSection.getSourceName()); } - String javadoc = javadocElement.get().description(); + String javadoc = JavadocTransformer.transform(javadocElement.get().description(), javadocElement.get().format(), + javadocFormat()); if (javadoc == null || javadoc.isBlank()) { throw new IllegalStateException( "Couldn't find section title for: " + configSection.getSourceClass() + "#" + configSection.getSourceName()); @@ -229,7 +234,7 @@ private static String trimFinalDot(String javadoc) { return javadoc.substring(0, dotIndex); } - protected abstract String javadoc(JavadocElement javadocElement); + protected abstract JavadocFormat javadocFormat(); protected abstract String moreInformationAboutType(String anchorRoot, String type); diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java index e0d834eb1c225..dab5e3d1aa1d3 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java @@ -1,7 +1,7 @@ package io.quarkus.maven.config.doc.generator; import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; -import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; final class AsciidocFormatter extends AbstractFormatter { @@ -12,6 +12,11 @@ final class AsciidocFormatter extends AbstractFormatter { super(javadocRepository, enableEnumTooltips); } + @Override + protected JavadocFormat javadocFormat() { + return JavadocFormat.ASCIIDOC; + } + protected String moreInformationAboutType(String anchorRoot, String type) { return String.format(MORE_INFO_ABOUT_TYPE_FORMAT, anchorRoot + "-{summaryTableId}", type); } @@ -34,9 +39,4 @@ private String cleanTooltipContent(String tooltipContent) { protected String link(String href, String description) { return String.format("link:%s[%s]", href, description); } - - @Override - protected String javadoc(JavadocElements.JavadocElement javadocElement) { - return javadocElement.description(); - } } diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java index eaf13fda73cf7..425e7481134fe 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java @@ -2,7 +2,7 @@ import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; -import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; final class MarkdownFormatter extends AbstractFormatter { @@ -12,6 +12,11 @@ final class MarkdownFormatter extends AbstractFormatter { super(javadocRepository, enableEnumTooltips); } + @Override + protected JavadocFormat javadocFormat() { + return JavadocFormat.MARKDOWN; + } + @Override public String formatSectionTitle(ConfigSection configSection) { // markdown only has 6 heading levels @@ -40,9 +45,4 @@ protected String tooltip(String value, String javadocDescription) { // we don't have tooltip support in Markdown return "`" + value + "`"; } - - @Override - protected String javadoc(JavadocElements.JavadocElement javadocElement) { - return javadocElement.rawJavadoc(); - } }