Repository: asciidoctor/asciidoctor-maven-plugin (java)

Issue:
Support Code blocks titles in asciidoctor-parser-doxia-module
**What is this issue about?**
<!-- Update "[ ]" to "[x]" to check a box -->

- [ ] Bug report 🪲
- [x] Feature request 🏎
- [ ] Question ❓ Consider dropping it first in the [Project Chat](https://asciidoctor.zulipchat.com/#narrow/stream/users)


**Description**

Add support for titles in code blocks:

![image](https://github.com/user-attachments/assets/b0d7849a-ddf2-4dd0-8417-27c998f4e52c)

This is not supported in `maven-fluido-skin` (https://maven.apache.org/skins/maven-fluido-skin/sample.html) but I was experimenting and I could add this really easily embedding some styles.

Pull request description:
Add support for code blocks titles in asciidoctor-parser-doxia-module
**What kind of change does this PR introduce?** (check at least one)
<!-- Update "[ ]" to "[x]" to check a box -->

- [ ] Bugfix
- [x] Feature
- [ ] Documentation
- [ ] Refactor
- [ ] Build improvement
- [ ] Other (please describe)


**What is the goal of this pull request?**

Adding non Doxia/fluido supported feature.

**Are there any alternative ways to implement this?**

For now we embed some styles. When we add more "un-supported" features we'd need to create a fluid compatible extra CSS and look into how to inject it and how to define custom CSS classes without breaking Fluido Skin.

**Are there any implications of this pull request? Anything a user must know?**

This adds an AsciiDoc feature that is not supported natively by Doxia or Fluido skin

**Is it related to an existing issue?**
<!-- If Yes, please add a line of the form: `Fixes #Issue` --> 
- [x] Yes
- [ ] No

Fixes #935



Patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 73c1e84..e5d18bf 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -22,6 +22,7 @@ Improvements::
 
   * Added support for AsciidoctorJ v3.0.0 (#651)
   * Add compatibility with maven-site-plugin v3.20.0 and Doxia v2.0.0 (#933)
+  * Add support for code blocks titles in asciidoctor-parser-doxia-module (#935)
 
 Build / Infrastructure::
 
diff --git a/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/src/site/asciidoc/sample.adoc b/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/src/site/asciidoc/sample.adoc
index 8d18df9..880c00a 100644
--- a/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/src/site/asciidoc/sample.adoc
+++ b/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/src/site/asciidoc/sample.adoc
@@ -39,6 +39,7 @@ image::images/asciidoctor-logo.png[Asciidoctor is awesome]
 === Code blocks
 
 [source,ruby]
+.Ruby example
 ----
 puts "Hello, World!"
 ----
diff --git a/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/validate.groovy b/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/validate.groovy
index 854b71d..6d7d5b5 100644
--- a/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/validate.groovy
+++ b/asciidoctor-parser-doxia-module/src/it/maven-site-plugin/validate.groovy
@@ -35,6 +35,8 @@ new HtmlAsserter(htmlContent).with { asserter ->
     asserter.containsLiteral("This is a literal.")
 
     asserter.containsSectionTitle("Code blocks", 3)
+    asserter.containsCodeBlock("Ruby example")
+    asserter.containsCodeBlock(null) // Java example without title
 
     asserter.containsSectionTitle("Lists", 3)
 
@@ -210,6 +212,18 @@ class HtmlAsserter {
         lastAssertionCursor = end
     }
 
+    void containsCodeBlock(String title) {
+        def blockKey = "<div class=\"source\">"
+        def found = find(blockKey)
+        assertFound("Code blockKey", blockKey, found)
+
+        if (title != null) {
+            def titleKey = "<div style=\"color:"
+            def foundTitle = find(titleKey)
+            assertFound("Code blockKey title", title, foundTitle)
+        }
+    }
+
     void assertTableCaption(String htmlBlock, String caption) {
         def start = htmlBlock.indexOf("<caption>") + "<caption>".length()
         def end = htmlBlock.indexOf("</caption>")
diff --git a/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/parser/processors/ListingNodeProcessor.java b/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/parser/processors/ListingNodeProcessor.java
index 4e9c40d..37e6ce0 100644
--- a/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/parser/processors/ListingNodeProcessor.java
+++ b/asciidoctor-parser-doxia-module/src/main/java/org/asciidoctor/maven/site/parser/processors/ListingNodeProcessor.java
@@ -3,6 +3,7 @@ package org.asciidoctor.maven.site.parser.processors;
 import org.apache.maven.doxia.sink.Sink;
 import org.asciidoctor.ast.StructuralNode;
 import org.asciidoctor.jruby.ast.impl.BlockImpl;
+import org.asciidoctor.maven.commons.StringUtils;
 import org.asciidoctor.maven.site.parser.NodeProcessor;
 
 import static org.asciidoctor.maven.commons.StringUtils.isNotBlank;
@@ -41,7 +42,7 @@ public class ListingNodeProcessor extends AbstractSinkNodeProcessor implements N
     public void process(StructuralNode node) {
         final StringBuilder contentBuilder = new StringBuilder();
         String language = (String) node.getAttribute("language");
-        String style = (String) node.getAttribute("style");
+        String style = node.getStyle();
 
         boolean isSourceBlock = isSourceBlock(language, style);
 
@@ -49,6 +50,11 @@ public class ListingNodeProcessor extends AbstractSinkNodeProcessor implements N
             // source class triggers prettify auto-detection
             contentBuilder.append("<div class=\"source\">");
 
+            final String title = node.getTitle();
+            if (StringUtils.isNotBlank(title)) {
+                contentBuilder.append("<div style=\"color: #7a2518; margin-bottom: .25em;\" >" + title + "</div>");
+            }
+
             contentBuilder.append("<pre class=\"")
                     .append(FLUIDO_SKIN_SOURCE_HIGHLIGHTER);
             if (isLinenumsEnabled(node))
diff --git a/docs/modules/site-integration/pages/parser-module-setup-and-configuration.adoc b/docs/modules/site-integration/pages/parser-module-setup-and-configuration.adoc
index e29976c..031c7b7 100644
--- a/docs/modules/site-integration/pages/parser-module-setup-and-configuration.adoc
+++ b/docs/modules/site-integration/pages/parser-module-setup-and-configuration.adoc
@@ -186,6 +186,7 @@ NOTE: Unlike in Asciidoctor lists, descriptions are not surrounded by `<p>` and
 
 * Code blocks with source-highlighting using https://maven.apache.org/skins/maven-fluido-skin/#source-code-line-numbers[Fluido Skin Pretiffy].
 ** Support for numbered lines with `linenums`
+** Support for AsciiDoc titles
 
 * Literal blocks
 * Quotes


Test patch:
diff --git a/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/parser/processors/ListingNodeProcessorTest.java b/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/parser/processors/ListingNodeProcessorTest.java
index 19205f9..97f49c4 100644
--- a/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/parser/processors/ListingNodeProcessorTest.java
+++ b/asciidoctor-parser-doxia-module/src/test/java/org/asciidoctor/maven/site/parser/processors/ListingNodeProcessorTest.java
@@ -10,6 +10,7 @@ import org.asciidoctor.maven.site.parser.NodeProcessor;
 import org.asciidoctor.maven.site.parser.processors.test.NodeProcessorTest;
 import org.junit.jupiter.api.Test;
 
+import static org.asciidoctor.maven.commons.StringUtils.isNotBlank;
 import static org.asciidoctor.maven.site.parser.processors.test.StringTestUtils.clean;
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -31,6 +32,17 @@ class ListingNodeProcessorTest {
             .isEqualTo(expectedHtmlCodeBlock());
     }
 
+    @Test
+    void should_convert_full_source_block_with_caption() {
+        final String title = "Java example";
+        String content = buildDocument(title, "[source,java]");
+
+        String html = process(content);
+
+        assertThat(html)
+            .isEqualTo(expectedHtmlCodeBlock(title));
+    }
+
     @Test
     void should_convert_shorthand_source_block() {
         String content = documentWithShorthandSourceBlock();
@@ -41,15 +53,6 @@ class ListingNodeProcessorTest {
             .isEqualTo(expectedHtmlCodeBlock());
     }
 
-    private static String expectedHtmlCodeBlock() {
-        // Actual styling is added in JS by prettify
-        return "<div class=\"source\"><pre class=\"prettyprint\"><code>class HelloWorldLanguage {" +
-            "    public static void main(String[] args) {" +
-            "        System.out.println(\"Hello, World!\");" +
-            "    }" +
-            "}</code></pre></div>";
-    }
-
     @Test
     void should_convert_full_source_block_with_line_numbers_attribute() {
         String content = buildDocument("[source,java,linenums]");
@@ -97,8 +100,13 @@ class ListingNodeProcessorTest {
     }
 
     private static String buildDocument(String blockDefinition) {
+        return buildDocument(null, blockDefinition);
+    }
+
+    private static String buildDocument(String title, String blockDefinition) {
         return "= Document tile\n\n"
             + "== Section\n\n"
+            + buildTitle(title)
             + blockDefinition + "\n" +
             "----\n" +
             "class HelloWorldLanguage {\n" +
@@ -109,6 +117,31 @@ class ListingNodeProcessorTest {
             "----\n";
     }
 
+    private static String buildTitle(String title) {
+        if (isNotBlank(title))
+            return "." + title + "\n";
+        return "";
+    }
+
+    private static String expectedHtmlCodeBlock() {
+        return expectedHtmlCodeBlock(null);
+    }
+
+    private static String expectedHtmlCodeBlock(String title) {
+        // Actual styling is added in JS by prettify
+        return "<div class=\"source\">" +
+            (isNotBlank(title) ? expectedTitle(title) : "") +
+            "<pre class=\"prettyprint\"><code>class HelloWorldLanguage {" +
+            "    public static void main(String[] args) {" +
+            "        System.out.println(\"Hello, World!\");" +
+            "    }" +
+            "}</code></pre></div>";
+    }
+
+    private static String expectedTitle(String title) {
+        return "<div style=\"color: #7a2518; margin-bottom: .25em;\" >" + title + "</div>";
+    }
+
     private String process(String content) {
         StructuralNode node = asciidoctor.load(content, Options.builder().build())
             .findBy(Collections.singletonMap("context", ":listing"))