Croway commented on code in PR #27839:
URL: https://github.com/apache/camel/pull/27839#discussion_r4296592178


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java:
##########
@@ -103,11 +103,9 @@ static void register(Consumer<ToolDescriptor> registry) {
                         "common (default: no deprecated or advanced), 
required, all or false", false)
                 .param("includeHeaders", "boolean", "Include the message 
headers of a component (default false)", false)
                 .param("includeDoc", "boolean", "Include the full AsciiDoc 
page (default false)", false)
-                .param("docPage", "string", "simple doc sub-page to return as 
text (functions, operators, ognl, advanced)",
-                        false)
-                .param("optionsFilter", "string",
-                        "Keyword to match in option names or descriptions; for 
an EIP whose own options do not match, the options of its elements are searched 
(nestedOptions)",
-                        false)
+                .param("docPage", "string", CatalogDocs.DOC_PAGE_DESC, false)
+                .param("optionsFilter", "string", 
CatalogDocs.OPTIONS_FILTER_DESC, false)
+                .param("limit", "integer", CatalogDocs.OPTIONS_LIMIT_DESC, 
false)

Review Comment:
   Measured again after rebasing on main: core ~5064 (budget 5000), core+groups 
~5843 (5750), full ~10508 (10500), core+semantic ~6193 (6100). I tightened the 
`docPage`, `optionsFilter` and `limit` descriptions and the full-key sentence 
without dropping anything a model needs (~23 tokens saved), then raised the 
budgets in `AiPanelPromptBudgetTest`, with a comment on each: core 5100 
(measured ~5041), core+groups 5900 (~5820), full 10600 (~10485), core+semantic 
6250 (~6170). Full+semantic is ~10822, under its 11000 limit, so that one is 
unchanged. All 6 tests pass.
   
   _Claude Code on behalf of Croway_
   



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java:
##########
@@ -1434,9 +1651,396 @@ private static JsonObject componentDoc(
             result.put("docPages", new JsonArray(docPages));
             result.put("docPagesHint", pagesHint(docPages, "page"));
         }
+        if (!how.sections().isEmpty()) {
+            // what the excerpt leaves out is often what decides correctness 
(the mail useHeader* options, the xslt
+            // header parameters), so the page says what else it has, the 
sections with the filter term first
+            JsonArray titles = sectionTitles(how.sections(), filter);
+            result.put("sections", titles);
+            int matching = filter != null ? sectionsContaining(how.sections(), 
filter) : 0;
+            result.put("sectionsHint", "docPage=<section title> returns that 
section of the page as text"
+                                       + (matching > 0
+                                               ? "; the first " + matching + " 
contain '" + filter + "'"
+                                               : ""));
+        }
+        String mainGroup = MAIN_CONFIG_GROUPS.get(model.getScheme());
+        if (mainGroup != null) {
+            putMainConfig(result, how, mainGroup);
+        }
         return result;
     }
 
+    /**
+     * The options of a component: those the scope accepts and the filter 
matches, the required ones first, then the
+     * endpoint options (what a URI takes) and the component options, with 
their application.properties key. Without a
+     * filter only the first {@link #DEFAULT_OPTIONS_LIMIT} are listed in full 
and the rest by name. A filter that
+     * matches nothing gets a note and the closest option names, and the 
prefix options are always said, as they take
+     * keys no option list names (CAMEL-25541).
+     */
+    private static void putComponentOptions(
+            JsonObject result, ComponentModel model, ComponentAnswer how, 
OptionScope scope) {
+        String filter = how.filter();
+        List<JsonObject> required = new ArrayList<>();
+        List<JsonObject> byName = new ArrayList<>();
+        List<JsonObject> others = new ArrayList<>();
+        int omitted = 0;
+        for (String optionScope : List.of("endpoint", "component")) {
+            List<? extends BaseOptionModel> options
+                    = "endpoint".equals(optionScope) ? 
model.getEndpointOptions() : model.getComponentOptions();
+            if (options == null) {
+                continue;
+            }
+            for (BaseOptionModel opt : options) {
+                if (!matchesOptionFilter(opt, filter)) {
+                    continue;
+                }
+                if (!scope.accepts(opt, filter)) {
+                    omitted++;
+                    continue;
+                }
+                JsonObject o = optionToJson(opt, optionScope);
+                if ("component".equals(optionScope)) {
+                    // the only valid application.properties key of an option: 
camel.component.<scheme>.* sets a
+                    // component option, never an endpoint one
+                    o.put("configKey", "camel.component." + model.getScheme() 
+ "." + opt.getName());
+                }
+                if (opt.isRequired()) {
+                    required.add(o);
+                } else if (filter != null && 
opt.getName().toLowerCase(Locale.ROOT).contains(filter)) {
+                    // a name match before a match in a description
+                    byName.add(o);
+                } else {
+                    others.add(o);
+                }
+            }
+        }
+        List<JsonObject> ordered = new ArrayList<>(required);
+        ordered.addAll(byName);
+        ordered.addAll(others);
+        int max = how.limit() >= 0 ? how.limit() : filter == null ? 
DEFAULT_OPTIONS_LIMIT : Integer.MAX_VALUE;
+        // the required options are always listed
+        int listed = Math.min(ordered.size(), Math.max(max, required.size()));
+        putOptions(result, new JsonArray(ordered.subList(0, listed)), omitted, 
scope);
+        result.put("matchedOptions", ordered.size());
+        if (listed < ordered.size()) {
+            Set<String> shown = new HashSet<>();
+            ordered.subList(0, listed).forEach(o -> 
shown.add(o.getString("name")));
+            Set<String> rest = new LinkedHashSet<>();
+            for (JsonObject o : ordered.subList(listed, ordered.size())) {
+                if (!shown.contains(o.getString("name"))) {
+                    rest.add(o.getString("name"));
+                }

Review Comment:
   Done as you suggested. When an endpoint option has a component option with 
the same name, it is listed once, with `scope: "endpoint,component"` and its 
`configKey`. The component twin is no longer listed separately, so the `shown` 
filtering is gone and `matchedOptions` counts each name once. The kafka default 
answer now has `brokers` with `configKey: camel.component.kafka.brokers`. When 
the endpoint twin is outside the requested scope (timer's `bridgeErrorHandler` 
is advanced on the endpoint only), the component option is listed on its own 
and is not counted as omitted. A new test checks kafka's `brokers` configKey 
and that the listed and named options have no duplicates and add up to 
`matchedOptions`. The upgrade guide and the MCP page describe the merged scope.
   
   _Claude Code on behalf of Croway_
   



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java:
##########
@@ -1434,9 +1651,396 @@ private static JsonObject componentDoc(
             result.put("docPages", new JsonArray(docPages));
             result.put("docPagesHint", pagesHint(docPages, "page"));
         }
+        if (!how.sections().isEmpty()) {
+            // what the excerpt leaves out is often what decides correctness 
(the mail useHeader* options, the xslt
+            // header parameters), so the page says what else it has, the 
sections with the filter term first
+            JsonArray titles = sectionTitles(how.sections(), filter);
+            result.put("sections", titles);
+            int matching = filter != null ? sectionsContaining(how.sections(), 
filter) : 0;
+            result.put("sectionsHint", "docPage=<section title> returns that 
section of the page as text"
+                                       + (matching > 0
+                                               ? "; the first " + matching + " 
contain '" + filter + "'"
+                                               : ""));
+        }
+        String mainGroup = MAIN_CONFIG_GROUPS.get(model.getScheme());
+        if (mainGroup != null) {
+            putMainConfig(result, how, mainGroup);
+        }
         return result;
     }
 
+    /**
+     * The options of a component: those the scope accepts and the filter 
matches, the required ones first, then the
+     * endpoint options (what a URI takes) and the component options, with 
their application.properties key. Without a
+     * filter only the first {@link #DEFAULT_OPTIONS_LIMIT} are listed in full 
and the rest by name. A filter that
+     * matches nothing gets a note and the closest option names, and the 
prefix options are always said, as they take
+     * keys no option list names (CAMEL-25541).
+     */
+    private static void putComponentOptions(
+            JsonObject result, ComponentModel model, ComponentAnswer how, 
OptionScope scope) {
+        String filter = how.filter();
+        List<JsonObject> required = new ArrayList<>();
+        List<JsonObject> byName = new ArrayList<>();
+        List<JsonObject> others = new ArrayList<>();
+        int omitted = 0;
+        for (String optionScope : List.of("endpoint", "component")) {
+            List<? extends BaseOptionModel> options
+                    = "endpoint".equals(optionScope) ? 
model.getEndpointOptions() : model.getComponentOptions();
+            if (options == null) {
+                continue;
+            }
+            for (BaseOptionModel opt : options) {
+                if (!matchesOptionFilter(opt, filter)) {
+                    continue;
+                }
+                if (!scope.accepts(opt, filter)) {
+                    omitted++;
+                    continue;
+                }
+                JsonObject o = optionToJson(opt, optionScope);
+                if ("component".equals(optionScope)) {
+                    // the only valid application.properties key of an option: 
camel.component.<scheme>.* sets a
+                    // component option, never an endpoint one
+                    o.put("configKey", "camel.component." + model.getScheme() 
+ "." + opt.getName());
+                }
+                if (opt.isRequired()) {
+                    required.add(o);
+                } else if (filter != null && 
opt.getName().toLowerCase(Locale.ROOT).contains(filter)) {
+                    // a name match before a match in a description
+                    byName.add(o);
+                } else {
+                    others.add(o);
+                }
+            }
+        }
+        List<JsonObject> ordered = new ArrayList<>(required);
+        ordered.addAll(byName);
+        ordered.addAll(others);
+        int max = how.limit() >= 0 ? how.limit() : filter == null ? 
DEFAULT_OPTIONS_LIMIT : Integer.MAX_VALUE;
+        // the required options are always listed
+        int listed = Math.min(ordered.size(), Math.max(max, required.size()));
+        putOptions(result, new JsonArray(ordered.subList(0, listed)), omitted, 
scope);
+        result.put("matchedOptions", ordered.size());
+        if (listed < ordered.size()) {
+            Set<String> shown = new HashSet<>();
+            ordered.subList(0, listed).forEach(o -> 
shown.add(o.getString("name")));
+            Set<String> rest = new LinkedHashSet<>();
+            for (JsonObject o : ordered.subList(listed, ordered.size())) {
+                if (!shown.contains(o.getString("name"))) {
+                    rest.add(o.getString("name"));
+                }
+            }
+            result.put("truncated", true);
+            result.put("listedOptions", listed);
+            result.put("moreOptions", new JsonArray(new ArrayList<>(rest)));
+            String hint = listed + " of " + ordered.size() + " options listed 
in full (required first, then the"
+                          + " endpoint options), the others by name in 
moreOptions; optionsFilter=<word or option name>"
+                          + " returns the matching ones in full, 
includeOptions=all with limit=<n> lists more";
+            String omittedHint = result.getString("optionsHint");
+            result.put("optionsHint", omittedHint != null ? hint + "; " + 
omittedHint : hint);
+        }
+        if (filter != null && ordered.isEmpty() && omitted == 0) {
+            // an empty list read as "not supported" (smtp + tls, xslt + 
param): say that nothing matched, and where
+            // else to look
+            result.put("note", "no option matches '" + filter + "': see 
closestOptions, prefixOptions and the"
+                               + " sections of the page below 
(docPage=<section title> returns one)");
+            JsonArray closest = closestOptions(model, filter);
+            if (!closest.isEmpty()) {
+                result.put("closestOptions", closest);
+            }
+        }
+        JsonArray prefixOptions = prefixOptions(model);
+        if (!prefixOptions.isEmpty()) {
+            result.put("prefixOptions", prefixOptions);
+        }
+    }
+
+    /**
+     * The options whose name, or a word of it, is closest to the filter by 
edit distance, with their group: ssl for a
+     * mistyped tsl.
+     */
+    private static JsonArray closestOptions(ComponentModel model, String 
filter) {
+        Map<String, List<BaseOptionModel>> byWord = new LinkedHashMap<>();
+        Map<BaseOptionModel, String> scopes = new LinkedHashMap<>();
+        for (String optionScope : List.of("endpoint", "component")) {
+            List<? extends BaseOptionModel> options
+                    = "endpoint".equals(optionScope) ? 
model.getEndpointOptions() : model.getComponentOptions();
+            if (options == null) {
+                continue;
+            }
+            for (BaseOptionModel opt : options) {
+                scopes.putIfAbsent(opt, optionScope);
+                byWord.computeIfAbsent(opt.getName().toLowerCase(Locale.ROOT), 
k -> new ArrayList<>()).add(opt);
+                for (String word : 
opt.getName().split("(?<=[a-z0-9])(?=[A-Z])")) {
+                    if (word.length() > 1) {
+                        byWord.computeIfAbsent(word.toLowerCase(Locale.ROOT), 
k -> new ArrayList<>()).add(opt);
+                    }
+                }
+            }
+        }
+        JsonArray answer = new JsonArray();
+        Set<String> seen = new HashSet<>();
+        for (String word : 
EditDistanceSuggestionStrategy.suggest(byWord.keySet(), filter, 5)) {
+            for (BaseOptionModel opt : byWord.get(word)) {
+                if (answer.size() < 5 && seen.add(opt.getName())) {
+                    JsonObject o = new JsonObject();
+                    o.put("name", opt.getName());
+                    if (opt.getGroup() != null) {
+                        o.put("group", opt.getGroup());
+                    }
+                    o.put("scope", scopes.get(opt));
+                    answer.add(o);
+                }
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * The multi-value options of a component, which take every URI parameter 
with their prefix: any mail.* parameter of
+     * smtp goes to JavaMail (mail.smtp.starttls.enable=true), which no option 
is named after.
+     */
+    private static JsonArray prefixOptions(ComponentModel model) {
+        JsonArray answer = new JsonArray();
+        Set<String> seen = new HashSet<>();
+        if (model.getEndpointOptions() == null) {
+            return answer;
+        }
+        for (BaseOptionModel opt : model.getEndpointOptions()) {
+            if (opt.isMultiValue() && opt.getPrefix() != null && 
seen.add(opt.getName())) {
+                JsonObject o = new JsonObject();
+                o.put("name", opt.getName());
+                o.put("prefix", opt.getPrefix());
+                o.put("usage", "any " + opt.getPrefix() + "* URI parameter is 
passed on as " + opt.getName() + ": ?"
+                               + opt.getPrefix() + "<key>=<value>");
+                answer.add(o);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Where a component that runs on a server of camel run or Camel Main gets 
its settings: the HTTP port of
+     * platform-http is camel.server.port, which an agent searched the 
component's options for (CAMEL-25541).
+     */
+    private static void putMainConfig(JsonObject result, ComponentAnswer how, 
String group) {
+        result.put("mainConfig", "with camel run or Camel Main the HTTP server 
is configured by the " + group
+                                 + ".* keys of application.properties, not by 
an option of this component:"
+                                 + " camel.server.enabled=true and 
camel.server.port=8080 (camel run --port=8080 sets"
+                                 + " both); camel_catalog_doc name=" + group + 
" lists the keys");

Review Comment:
   I checked the code: `DependencyDownloaderComponentResolver` calls 
`MainHttpServerFactory.setupHttpServer` for `PlatformHttpComponent`, and 
`configureHttpServer` applies `camel.server.*` and skips `enabled`. 
`mainConfig` now reads: "... with camel run it starts when a route uses 
platform-http, on port 8080 (camel.server.port or camel run --port changes it); 
with Camel Main set camel.server.enabled=true and camel.server.port; ...". The 
test checks both halves.
   
   _Claude Code on behalf of Croway_
   



##########
components/camel-platform-http/src/main/docs/platform-http-component.adoc:
##########
@@ -40,6 +40,11 @@ include::partial$component-endpoint-headers.adoc[]
 To use Platform HTTP, a provider (engine) is required to be available on the 
classpath.
 The purpose is to have drivers for different runtimes such as Quarkus, or 
Spring Boot.
 
+With Camel Main and `camel run`, the provider is the embedded HTTP server of 
`camel-platform-http-main`,
+which is disabled by default. Enable it and set its port in 
`application.properties` with
+`camel.server.enabled=true` and `camel.server.port=8080` (`camel run 
--port=8080` sets both).
+See xref:others:platform-http-main.adoc[Platform HTTP Main] for the other 
`camel.server.*` options.

Review Comment:
   Rewritten along your wording. With `camel run`, the server of 
`camel-platform-http-main` starts when a route uses platform-http, on port 8080 
(`camel.server.port` or `camel run --port` changes it). With Camel Main it is 
disabled by default: enable it with `camel.server.enabled=true` and set 
`camel.server.port`. The catalog mirror is regenerated and is byte-identical to 
the source page.
   
   _Claude Code on behalf of Croway_
   



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java:
##########
@@ -1434,9 +1651,396 @@ private static JsonObject componentDoc(
             result.put("docPages", new JsonArray(docPages));
             result.put("docPagesHint", pagesHint(docPages, "page"));
         }
+        if (!how.sections().isEmpty()) {
+            // what the excerpt leaves out is often what decides correctness 
(the mail useHeader* options, the xslt
+            // header parameters), so the page says what else it has, the 
sections with the filter term first
+            JsonArray titles = sectionTitles(how.sections(), filter);
+            result.put("sections", titles);
+            int matching = filter != null ? sectionsContaining(how.sections(), 
filter) : 0;
+            result.put("sectionsHint", "docPage=<section title> returns that 
section of the page as text"
+                                       + (matching > 0
+                                               ? "; the first " + matching + " 
contain '" + filter + "'"
+                                               : ""));
+        }
+        String mainGroup = MAIN_CONFIG_GROUPS.get(model.getScheme());
+        if (mainGroup != null) {
+            putMainConfig(result, how, mainGroup);
+        }
         return result;
     }
 
+    /**
+     * The options of a component: those the scope accepts and the filter 
matches, the required ones first, then the
+     * endpoint options (what a URI takes) and the component options, with 
their application.properties key. Without a
+     * filter only the first {@link #DEFAULT_OPTIONS_LIMIT} are listed in full 
and the rest by name. A filter that
+     * matches nothing gets a note and the closest option names, and the 
prefix options are always said, as they take
+     * keys no option list names (CAMEL-25541).
+     */
+    private static void putComponentOptions(
+            JsonObject result, ComponentModel model, ComponentAnswer how, 
OptionScope scope) {
+        String filter = how.filter();
+        List<JsonObject> required = new ArrayList<>();
+        List<JsonObject> byName = new ArrayList<>();
+        List<JsonObject> others = new ArrayList<>();
+        int omitted = 0;
+        for (String optionScope : List.of("endpoint", "component")) {
+            List<? extends BaseOptionModel> options
+                    = "endpoint".equals(optionScope) ? 
model.getEndpointOptions() : model.getComponentOptions();
+            if (options == null) {
+                continue;
+            }
+            for (BaseOptionModel opt : options) {
+                if (!matchesOptionFilter(opt, filter)) {
+                    continue;
+                }
+                if (!scope.accepts(opt, filter)) {
+                    omitted++;
+                    continue;
+                }
+                JsonObject o = optionToJson(opt, optionScope);
+                if ("component".equals(optionScope)) {
+                    // the only valid application.properties key of an option: 
camel.component.<scheme>.* sets a
+                    // component option, never an endpoint one
+                    o.put("configKey", "camel.component." + model.getScheme() 
+ "." + opt.getName());
+                }
+                if (opt.isRequired()) {
+                    required.add(o);
+                } else if (filter != null && 
opt.getName().toLowerCase(Locale.ROOT).contains(filter)) {
+                    // a name match before a match in a description
+                    byName.add(o);
+                } else {
+                    others.add(o);
+                }
+            }
+        }
+        List<JsonObject> ordered = new ArrayList<>(required);
+        ordered.addAll(byName);
+        ordered.addAll(others);
+        int max = how.limit() >= 0 ? how.limit() : filter == null ? 
DEFAULT_OPTIONS_LIMIT : Integer.MAX_VALUE;
+        // the required options are always listed
+        int listed = Math.min(ordered.size(), Math.max(max, required.size()));
+        putOptions(result, new JsonArray(ordered.subList(0, listed)), omitted, 
scope);
+        result.put("matchedOptions", ordered.size());
+        if (listed < ordered.size()) {
+            Set<String> shown = new HashSet<>();
+            ordered.subList(0, listed).forEach(o -> 
shown.add(o.getString("name")));
+            Set<String> rest = new LinkedHashSet<>();
+            for (JsonObject o : ordered.subList(listed, ordered.size())) {
+                if (!shown.contains(o.getString("name"))) {
+                    rest.add(o.getString("name"));
+                }
+            }
+            result.put("truncated", true);
+            result.put("listedOptions", listed);
+            result.put("moreOptions", new JsonArray(new ArrayList<>(rest)));
+            String hint = listed + " of " + ordered.size() + " options listed 
in full (required first, then the"
+                          + " endpoint options), the others by name in 
moreOptions; optionsFilter=<word or option name>"
+                          + " returns the matching ones in full, 
includeOptions=all with limit=<n> lists more";
+            String omittedHint = result.getString("optionsHint");
+            result.put("optionsHint", omittedHint != null ? hint + "; " + 
omittedHint : hint);
+        }
+        if (filter != null && ordered.isEmpty() && omitted == 0) {
+            // an empty list read as "not supported" (smtp + tls, xslt + 
param): say that nothing matched, and where
+            // else to look
+            result.put("note", "no option matches '" + filter + "': see 
closestOptions, prefixOptions and the"
+                               + " sections of the page below 
(docPage=<section title> returns one)");
+            JsonArray closest = closestOptions(model, filter);
+            if (!closest.isEmpty()) {
+                result.put("closestOptions", closest);
+            }
+        }
+        JsonArray prefixOptions = prefixOptions(model);
+        if (!prefixOptions.isEmpty()) {
+            result.put("prefixOptions", prefixOptions);
+        }
+    }
+
+    /**
+     * The options whose name, or a word of it, is closest to the filter by 
edit distance, with their group: ssl for a
+     * mistyped tsl.
+     */
+    private static JsonArray closestOptions(ComponentModel model, String 
filter) {
+        Map<String, List<BaseOptionModel>> byWord = new LinkedHashMap<>();
+        Map<BaseOptionModel, String> scopes = new LinkedHashMap<>();
+        for (String optionScope : List.of("endpoint", "component")) {
+            List<? extends BaseOptionModel> options
+                    = "endpoint".equals(optionScope) ? 
model.getEndpointOptions() : model.getComponentOptions();
+            if (options == null) {
+                continue;
+            }
+            for (BaseOptionModel opt : options) {
+                scopes.putIfAbsent(opt, optionScope);
+                byWord.computeIfAbsent(opt.getName().toLowerCase(Locale.ROOT), 
k -> new ArrayList<>()).add(opt);
+                for (String word : 
opt.getName().split("(?<=[a-z0-9])(?=[A-Z])")) {
+                    if (word.length() > 1) {
+                        byWord.computeIfAbsent(word.toLowerCase(Locale.ROOT), 
k -> new ArrayList<>()).add(opt);
+                    }
+                }
+            }
+        }
+        JsonArray answer = new JsonArray();
+        Set<String> seen = new HashSet<>();
+        for (String word : 
EditDistanceSuggestionStrategy.suggest(byWord.keySet(), filter, 5)) {
+            for (BaseOptionModel opt : byWord.get(word)) {
+                if (answer.size() < 5 && seen.add(opt.getName())) {
+                    JsonObject o = new JsonObject();
+                    o.put("name", opt.getName());
+                    if (opt.getGroup() != null) {
+                        o.put("group", opt.getGroup());
+                    }
+                    o.put("scope", scopes.get(opt));
+                    answer.add(o);
+                }
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * The multi-value options of a component, which take every URI parameter 
with their prefix: any mail.* parameter of
+     * smtp goes to JavaMail (mail.smtp.starttls.enable=true), which no option 
is named after.
+     */
+    private static JsonArray prefixOptions(ComponentModel model) {
+        JsonArray answer = new JsonArray();
+        Set<String> seen = new HashSet<>();
+        if (model.getEndpointOptions() == null) {
+            return answer;
+        }
+        for (BaseOptionModel opt : model.getEndpointOptions()) {
+            if (opt.isMultiValue() && opt.getPrefix() != null && 
seen.add(opt.getName())) {
+                JsonObject o = new JsonObject();
+                o.put("name", opt.getName());
+                o.put("prefix", opt.getPrefix());
+                o.put("usage", "any " + opt.getPrefix() + "* URI parameter is 
passed on as " + opt.getName() + ": ?"
+                               + opt.getPrefix() + "<key>=<value>");
+                answer.add(o);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Where a component that runs on a server of camel run or Camel Main gets 
its settings: the HTTP port of
+     * platform-http is camel.server.port, which an agent searched the 
component's options for (CAMEL-25541).
+     */
+    private static void putMainConfig(JsonObject result, ComponentAnswer how, 
String group) {
+        result.put("mainConfig", "with camel run or Camel Main the HTTP server 
is configured by the " + group
+                                 + ".* keys of application.properties, not by 
an option of this component:"
+                                 + " camel.server.enabled=true and 
camel.server.port=8080 (camel run --port=8080 sets"
+                                 + " both); camel_catalog_doc name=" + group + 
" lists the keys");
+        if (how.filter() == null) {
+            return;
+        }
+        JsonArray keys = new JsonArray();
+        try {
+            for (BaseOptionModel o : 
optionsUnder(how.catalog().mainModel().getOptions(), group)) {
+                if (matchesOptionFilter(o, how.filter())) {
+                    keys.add(mainOptionToJson(o));
+                }
+            }
+        } catch (Exception e) {
+            return;
+        }
+        if (!keys.isEmpty()) {
+            result.put("mainOptions", keys);
+        }
+    }
+
+    /**
+     * The name the documentation page of a component has: its scheme, else 
the artifact (smtp, imap and pop3 are on the
+     * mail page of camel-mail); null when there is none.
+     */
+    private static String componentDocName(CamelCatalog catalog, 
ComponentModel model) {
+        if (catalog.asciiDoc(model.getScheme() + "-component") != null) {
+            return model.getScheme();
+        }
+        String artifact = model.getArtifactId();
+        if (artifact != null && artifact.startsWith("camel-")) {
+            String name = artifact.substring("camel-".length());
+            if (catalog.asciiDoc(name + "-component") != null) {
+                return name;
+            }
+        }
+        return null;
+    }
+
+    private static String titleWords(String text) {
+        return text.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", " 
").strip();
+    }
+
+    /**
+     * A section of an AsciiDoc page: its title, its level (2 for ==), its 
text with its subsections, and its own text
+     * up to its first subsection.
+     */
+    record DocSection(String title, int level, String text, String own) {
+    }
+
+    private static final Pattern HEADING = Pattern.compile("^(={2,4}) 
+(\\S.*)$");
+
+    /** The sections of a page, from == to ====, in the order of the page; 
empty when there is no page. */
+    static List<DocSection> docSections(String adoc) {
+        List<DocSection> answer = new ArrayList<>();
+        if (adoc == null) {
+            return answer;
+        }
+        String[] lines = adoc.split("\n", -1);
+        List<Integer> starts = new ArrayList<>();
+        List<Integer> levels = new ArrayList<>();
+        List<String> titles = new ArrayList<>();
+        boolean fenced = false;
+        for (int i = 0; i < lines.length; i++) {
+            String trimmed = lines[i].strip();
+            if (trimmed.startsWith("----") || trimmed.equals("....")) {

Review Comment:
   Fixed. `docSections` now records the delimiter that opened a block and 
closes the block only on that same delimiter. Listing (`----`), literal 
(`....`), comment (`////`) and passthrough (`++++`) blocks are covered. A new 
test has a `....` block inside a `----` block and a `////` comment block, and 
the headings inside them are not sections. (The suggested patch would still 
have closed the outer block on `....`.)
   
   _Claude Code on behalf of Croway_
   



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java:
##########
@@ -250,6 +309,10 @@ public static JsonObject catalogDoc(
             // (jackson): both are answered, with the YAML shape in the answer 
(CAMEL-24898)
             String artifact = 
YAML_DATAFORMAT_NAMES.getOrDefault(name.toLowerCase(), name);
             DataFormatModel dm = catalog.dataFormatModel(artifact);
+            if (dm == null && ignoringCase(catalog.findDataFormatNames(), 
artifact) != null) {
+                artifact = ignoringCase(catalog.findDataFormatNames(), 
artifact);

Review Comment:
   Fixed: the result is computed once into a local.
   
   _Claude Code on behalf of Croway_
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to