This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-25040
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 459fe191f84b40f1458d920f4a1b4a9f4b428ca4
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Sep 26 20:56:08 2026 +0200

    CAMEL-25040: camel_catalog_doc carries the start of a component's 
documentation, not only its options
    
    The answer for a component was its description, syntax, Maven coordinates 
and
    option list. An option list can only say what can be set: syntax that is 
not an
    option is invisible in it. That a named parameter of the sql component is
    written :#name appears nowhere in its 15 KB of options -- 
allowNamedParameters
    says "Whether to allow using named parameters in the queries" and never 
what one
    looks like -- while the component's own page explains it in its first 80 
lines.
    That page was reachable only through includeDoc, which returns all of it 
and is
    off by default, so an author who does not already know the answer has no 
reason
    to ask for it.
    
    The answer now carries the page's prose from its first section, trimmed to 
1400
    characters, with a hint that includeDoc gives the rest. Left out: the title 
and
    attribute header, the Maven dependency stanza, and the include:: directives 
that
    pull in the generated option tables, which the answer already carries as
    options. A cross-reference is reduced to the words it links, so the excerpt
    reads as text rather than as AsciiDoc. Asking for the whole page does not 
also
    send the excerpt.
    
    For sql this puts ":#name_of_the_parameter", the lookup precedence and the
    $simple{} form in the answer, in 1377 characters.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    |  80 +++++++++++-
 .../core/commands/ai/CatalogDocExcerptTest.java    | 136 +++++++++++++++++++++
 2 files changed, 213 insertions(+), 3 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index 31a8a57878e2..7db9f6066114 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.regex.Pattern;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
@@ -101,6 +102,71 @@ public final class CatalogDocs {
      * @param  docPage        a language doc sub-page (simple: functions, 
operators, ognl, advanced) to return as text
      * @return                the JSON result, an {@code error} object when 
nothing matches
      */
+
+    /** How much of a component's documentation prose is carried in the answer 
by default (CAMEL-25040). */
+    static final int DOC_EXCERPT_BUDGET = 1400;
+
+    private static final Pattern XREF = 
Pattern.compile("xref:[^\\[]*\\[([^]]*)]");
+    private static final Pattern INTERNAL_REF = 
Pattern.compile("<<[^,>]*,([^>]*)>>");
+
+    /**
+     * The prose of a component's documentation page, trimmed to a budget, or 
null when the page has none.
+     * <p/>
+     * The option list says what can be set, and nothing else: syntax that is 
not an option is invisible in it. The
+     * {@code sql} component is the clearest case -- that a named parameter is 
written {@code :#name} appears nowhere in
+     * its options, only in its page, which an author who does not already 
know the answer has no reason to ask for
+     * (CAMEL-25040). So the page's own prose comes along, from its first 
section, which is where a component page
+     * explains its URI and the essentials.
+     * <p/>
+     * Left out: the title and attribute header, the Maven dependency stanza, 
and the {@code include::} directives that
+     * pull in the generated option tables, which the answer already carries 
as options. A cross-reference is reduced to
+     * the words it links, so the excerpt reads as text rather than as 
AsciiDoc.
+     */
+    static String docExcerpt(String asciiDoc, int budget) {
+        if (asciiDoc == null || asciiDoc.isBlank()) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        boolean started = false;
+        boolean fenced = false;
+        for (String line : asciiDoc.split("\n", -1)) {
+            String trimmed = line.strip();
+            if (!started) {
+                // the page's own header, its intro and the dependency stanza 
come before the first section, and the
+                // description and Maven coordinates of the answer already say 
what they say
+                started = trimmed.startsWith("== ");
+                if (!started) {
+                    continue;
+                }
+            }
+            if (trimmed.startsWith("include::") || 
trimmed.startsWith("ifdef::") || trimmed.startsWith("endif::")
+                    || trimmed.startsWith("ifndef::")) {
+                continue;
+            }
+            if (trimmed.startsWith("----")) {
+                fenced = !fenced;
+            } else if (!fenced) {
+                // asciidoc decoration that carries nothing on its own
+                if (trimmed.equals("====") || trimmed.equals("|===") || 
trimmed.startsWith("[tabs]")
+                        || trimmed.startsWith("[width=") || 
trimmed.startsWith("[cols=")
+                        || trimmed.startsWith("[source,") || 
trimmed.startsWith("[NOTE]")
+                        || trimmed.startsWith("[TIP]") || 
trimmed.startsWith("[IMPORTANT]")
+                        || trimmed.startsWith("[WARNING]") || 
trimmed.startsWith("[CAUTION]")
+                        || (trimmed.startsWith(":") && trimmed.indexOf(':', 1) 
> 0)) {
+                    continue;
+                }
+            }
+            String text = XREF.matcher(line).replaceAll("$1");
+            text = INTERNAL_REF.matcher(text).replaceAll("$1");
+            if (sb.length() + text.length() + 1 > budget) {
+                break;
+            }
+            sb.append(text).append('\n');
+        }
+        String answer = sb.toString().strip();
+        return answer.isEmpty() ? null : answer;
+    }
+
     public static JsonObject catalogDoc(
             CamelCatalog catalog, String name, String endpoint, String kind, 
String optionsFilter,
             String includeOptions, boolean includeHeaders, boolean includeDoc, 
String docPage) {
@@ -120,8 +186,10 @@ public final class CatalogDocs {
         if (kind == null || "component".equals(kind)) {
             ComponentModel cm = catalog.componentModel(name);
             if (cm != null) {
-                String doc = includeDoc ? catalog.asciiDoc(name + 
"-component") : null;
-                return componentDoc(cm, lowerFilter, scope, includeHeaders, 
doc);
+                String adoc = catalog.asciiDoc(name + "-component");
+                // the whole page when it was asked for, else its first 
section, which the options cannot say
+                return componentDoc(cm, lowerFilter, scope, includeHeaders, 
includeDoc ? adoc : null,
+                        includeDoc ? null : docExcerpt(adoc, 
DOC_EXCERPT_BUDGET));
             }
             JsonObject group = mainOptionsGroup(catalog, name);
             if (group != null) {
@@ -949,7 +1017,8 @@ public final class CatalogDocs {
     }
 
     private static JsonObject componentDoc(
-            ComponentModel model, String filter, OptionScope scope, boolean 
includeHeaders, String doc) {
+            ComponentModel model, String filter, OptionScope scope, boolean 
includeHeaders, String doc,
+            String docExcerpt) {
         JsonObject result = new JsonObject();
         result.put("kind", "component");
         result.put("name", model.getScheme());
@@ -1026,6 +1095,11 @@ public final class CatalogDocs {
         if (doc != null) {
             result.put("doc", doc);
         }
+        if (docExcerpt != null) {
+            // what the options cannot say: the syntax and the essentials, 
from the component's own page
+            result.put("documentation", docExcerpt);
+            result.put("documentationHint", "the start of the component's 
documentation page; includeDoc=true for all of it");
+        }
         return result;
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExcerptTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExcerptTest.java
new file mode 100644
index 000000000000..49076846941d
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExcerptTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25040: a component's answer carries the start of its documentation 
page, because the option list can only say
+ * what can be set. That a named parameter of the sql component is written 
{@code :#name} is not an option and appeared
+ * nowhere in the answer, only in a page an author who does not know the 
answer has no reason to ask for.
+ */
+class CatalogDocExcerptTest {
+
+    private static JsonObject catalogDoc(Map<String, Object> args) throws 
Exception {
+        Map<String, String> stringArgs = new HashMap<>();
+        args.forEach((k, v) -> stringArgs.put(k, String.valueOf(v)));
+        String json = String.valueOf(ToolRegistry.execute("camel_catalog_doc", 
new ToolContext(), stringArgs));
+        return (JsonObject) Jsoner.deserialize(json);
+    }
+
+    @Test
+    public void testTheSqlNamedParameterSyntaxIsInTheAnswer() throws Exception 
{
+        JsonObject answer = catalogDoc(Map.of("name", "sql"));
+        String documentation = answer.getString("documentation");
+        assertNotNull(documentation, "no documentation in: " + 
answer.toJson());
+
+        // the whole point: the syntax that is not an option
+        assertTrue(documentation.contains(":#name_of_the_parameter") || 
documentation.contains(":#myId"),
+                "the named parameter syntax is not in the excerpt:\n" + 
documentation);
+        assertNotNull(answer.getString("documentationHint"));
+    }
+
+    @Test
+    public void testTheExcerptIsProseAndNotAsciiDocPlumbing() throws Exception 
{
+        String documentation = catalogDoc(Map.of("name", 
"sql")).getString("documentation");
+
+        // the generated option tables are the answer's own options, not prose
+        assertFalse(documentation.contains("include::"), documentation);
+        // a cross-reference reads as the words it links
+        assertFalse(documentation.contains("xref:"), documentation);
+        // the title and attribute header of the page are not carried
+        assertFalse(documentation.contains(":doctitle:"), documentation);
+        assertFalse(documentation.contains(":artifactid:"), documentation);
+    }
+
+    @Test
+    public void testTheExcerptStaysWithinItsBudget() throws Exception {
+        for (String name : new String[] { "sql", "kafka", "timer", "file", 
"http" }) {
+            String documentation = catalogDoc(Map.of("name", 
name)).getString("documentation");
+            if (documentation != null) {
+                assertTrue(documentation.length() <= 
CatalogDocs.DOC_EXCERPT_BUDGET,
+                        name + " excerpt is " + documentation.length() + " 
chars");
+            }
+        }
+    }
+
+    @Test
+    public void testAskingForTheWholePageDoesNotAlsoSendTheExcerpt() throws 
Exception {
+        JsonObject answer = catalogDoc(Map.of("name", "sql", "includeDoc", 
"true"));
+        assertNotNull(answer.getString("doc"));
+        assertNull(answer.getString("documentation"), "the whole page is 
there, so the excerpt would repeat it");
+    }
+
+    @Test
+    public void testAComponentWithNoPageIsStillAnswered() {
+        // a page is not guaranteed; the answer must not depend on one
+        assertNull(CatalogDocs.docExcerpt(null, 
CatalogDocs.DOC_EXCERPT_BUDGET));
+        assertNull(CatalogDocs.docExcerpt("", CatalogDocs.DOC_EXCERPT_BUDGET));
+        assertNull(CatalogDocs.docExcerpt("= Title\n:shortname: x\n\nIntro 
with no sections.\n",
+                CatalogDocs.DOC_EXCERPT_BUDGET));
+    }
+
+    @Test
+    public void testTheExcerptStartsAtTheFirstSection() {
+        String page = """
+                = SQL Component
+                :doctitle: SQL
+                :shortname: sql
+
+                *Since Camel 1.4*
+
+                Intro prose that the description already says.
+
+                [source,xml]
+                ----
+                <dependency>camel-sql</dependency>
+                ----
+
+                == URI format
+
+                Use :#name for a named parameter.
+                """;
+        String excerpt = CatalogDocs.docExcerpt(page, 
CatalogDocs.DOC_EXCERPT_BUDGET);
+        assertTrue(excerpt.startsWith("== URI format"), excerpt);
+        assertTrue(excerpt.contains("Use :#name for a named parameter."), 
excerpt);
+        assertFalse(excerpt.contains("camel-sql</dependency>"), excerpt);
+        assertFalse(excerpt.contains("Intro prose"), excerpt);
+    }
+
+    @Test
+    public void testCodeInsideAFenceIsKept() {
+        String page = """
+                == URI format
+
+                ----
+                sql:select * from table where id=:#myId order by name[?options]
+                ----
+                """;
+        String excerpt = CatalogDocs.docExcerpt(page, 
CatalogDocs.DOC_EXCERPT_BUDGET);
+        assertTrue(excerpt.contains("sql:select * from table where 
id=:#myId"), excerpt);
+    }
+}

Reply via email to