@docsearch/js 5.0.0-beta.2 → 5.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
- /*! @docsearch/js 5.0.0-beta.2 (UNRELEASED c7e1a88) | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */
2
- import { DocSearchAIProps as DocSearchAIProps$1 } from "@docsearch/react/docsearchAi";
1
+ /*! @docsearch/js 5.0.0-beta.3 (UNRELEASED b044250) | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */
3
2
  import { InitialAskAiMessage } from "@docsearch/core";
3
+ import { HitComponentProps, ResultsFooterComponentProps } from "@docsearch/react";
4
+ import { DocSearchAIProps as DocSearchAIProps$1 } from "@docsearch/react/docsearchAi";
4
5
  //#region ../../node_modules/.bun/preact@11.0.0-beta.0/node_modules/preact/src/index.d.ts
5
6
  import JSX = JSXInternal;
6
7
  //#endregion
@@ -19,18 +20,49 @@ interface DocSearchCallbacks {
19
20
  onClose?: () => void;
20
21
  interceptAskAiEvent?: (initialMessage: InitialAskAiMessage) => boolean | void;
21
22
  }
22
- type DocSearchProps<TProps> = DocSearchCallbacks & Omit<TProps, 'onSidepanelClose' | 'onSidepanelOpen'> & {
23
+ type TemplateHelpers = {
24
+ html: typeof html;
25
+ };
26
+ type TemplateFnReturnType = JSX.Element | string | (() => JSX.Element) | null;
27
+ type HitComponentFn = (props: HitComponentProps, helpers: TemplateHelpers) => TemplateFnReturnType;
28
+ type ResultsFooterComponentFn = (props: ResultsFooterComponentProps, helpers: TemplateHelpers) => TemplateFnReturnType;
29
+ type FooterActionFn = (props: never, helpers: TemplateHelpers) => TemplateFnReturnType;
30
+ type DocSearchProps<TProps> = DocSearchCallbacks & Omit<TProps, 'onSidepanelClose' | 'onSidepanelOpen' | 'hitComponent' | 'resultsFooterComponent' | 'footerAction'> & {
23
31
  container: HTMLElement | string;
24
32
  environment?: typeof window;
33
+ /**
34
+ * Custom component to render an individual hit. Supports template patterns:
35
+ *
36
+ * - HTML strings with html helper: (props, { html }) => html`<div>...</div>`
37
+ * - JSX templates: (props) => <div>...</div>
38
+ * - Function-based templates: (props) => string | JSX.Element | Function.
39
+ */
40
+ hitComponent?: HitComponentFn;
41
+ /**
42
+ * Custom component rendered at the bottom of the results panel. Supports
43
+ * template patterns:
44
+ *
45
+ * - HTML strings with html helper: (props, { html }) => html`<div>...</div>`
46
+ * - JSX templates: (props) => <div>...</div>
47
+ * - Function-based templates: (props) => string | JSX.Element | Function.
48
+ */
49
+ resultsFooterComponent?: ResultsFooterComponentFn;
50
+ /**
51
+ * A custom action that can be rendered in the Modal's footer before the
52
+ * Algolia logo. The component will be rendered as a child of `<div
53
+ * className="DocSearch-Footer-Action" />`. Supports template patterns:
54
+ *
55
+ * - HTML strings with html helper: (props, { html }) => html`<div>...</div>`
56
+ * - JSX templates: (props) => <div>...</div>
57
+ * - Function-based templates: (props) => string | JSX.Element | Function.
58
+ */
59
+ footerAction?: FooterActionFn;
25
60
  };
26
61
  declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => JSX.Element;
27
- type TemplateHelpers = Record<string, unknown> & {
28
- html: typeof html;
29
- };
30
62
  //#endregion
31
63
  //#region src/docsearchAi.d.ts
32
64
  type DocSearchAIProps = DocSearchProps<DocSearchAIProps$1>;
33
65
  declare const docsearchAi: (allProps: DocSearchAIProps) => DocSearchInstance;
34
66
  //#endregion
35
- export { type DocSearchCallbacks, type DocSearchInstance, type DocSearchAIProps as DocSearchProps, type TemplateHelpers, docsearchAi as default };
67
+ export { type DocSearchCallbacks, type DocSearchInstance, type DocSearchAIProps as DocSearchProps, type FooterActionFn, type HitComponentFn, type ResultsFooterComponentFn, type TemplateHelpers, docsearchAi as default };
36
68
  //# sourceMappingURL=index.d.ts.map
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @docsearch/js 5.0.0-beta.2 (UNRELEASED c7e1a88) | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */
1
+ /*! @docsearch/js 5.0.0-beta.3 (UNRELEASED b044250) | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */
2
2
  //#region \0rolldown/runtime.js
3
3
  var __create = Object.create;
4
4
  var __defProp$1 = Object.defineProperty;
@@ -1188,7 +1188,7 @@ function DocSearchButtonKey({ reactsToKey, children }) {
1188
1188
 
1189
1189
  //#endregion
1190
1190
  //#region ../docsearch-react/dist/esm/version.js
1191
- const version$2 = "5.0.0-beta.2";
1191
+ const version$2 = "5.0.0-beta.3";
1192
1192
 
1193
1193
  //#endregion
1194
1194
  //#region \0@oxc-project+runtime@0.139.0/helpers/esm/OverloadYield.js
@@ -12496,7 +12496,7 @@ function useMenuParent() {
12496
12496
  }
12497
12497
 
12498
12498
  //#endregion
12499
- //#region ../docsearch-react/dist/esm/normalizeDocSearchIndexes-D8P3VEXa.js
12499
+ //#region ../docsearch-react/dist/esm/normalizeDocSearchIndexes-ChnVvoVt.js
12500
12500
  init_compat();
12501
12501
  function capitalize(text) {
12502
12502
  return `${text.charAt(0).toLocaleUpperCase()}${text.substring(1)}`;
@@ -12952,12 +12952,13 @@ function CommandIcon(props) {
12952
12952
  strokeWidth: "1.4"
12953
12953
  }, props.children));
12954
12954
  }
12955
- function Footer({ translations = {}, isAskAiActive = false }) {
12955
+ function Footer({ translations = {}, isAskAiActive = false, footerAction }) {
12956
12956
  const { selectText = "Select", selectKeyAriaLabel = "Enter key", submitQuestionText = "Submit question", navigateText = "Navigate", navigateUpKeyAriaLabel = "Arrow up", navigateDownKeyAriaLabel = "Arrow down", closeText = "Close", backToSearchText = "Back to search", closeKeyAriaLabel = "Escape key", poweredByText = "Powered by" } = translations;
12957
- return /* @__PURE__ */ En.createElement(En.Fragment, null, /* @__PURE__ */ En.createElement("div", { className: "DocSearch-Logo" }, /* @__PURE__ */ En.createElement(AlgoliaLogo, { translations: { poweredByText } })), /* @__PURE__ */ En.createElement("ul", { className: "DocSearch-Commands" }, /* @__PURE__ */ En.createElement("li", null, /* @__PURE__ */ En.createElement("kbd", { className: "DocSearch-Commands-Key" }, /* @__PURE__ */ En.createElement(CommandIcon, { ariaLabel: navigateDownKeyAriaLabel }, /* @__PURE__ */ En.createElement("path", { d: "M12 5v14" }), /* @__PURE__ */ En.createElement("path", { d: "m19 12-7 7-7-7" }))), /* @__PURE__ */ En.createElement("kbd", { className: "DocSearch-Commands-Key" }, /* @__PURE__ */ En.createElement(CommandIcon, { ariaLabel: navigateUpKeyAriaLabel }, /* @__PURE__ */ En.createElement("path", { d: "m5 12 7-7 7 7" }), /* @__PURE__ */ En.createElement("path", { d: "M12 19V5" }))), /* @__PURE__ */ En.createElement("span", { className: "DocSearch-Label" }, navigateText)), /* @__PURE__ */ En.createElement("li", null, /* @__PURE__ */ En.createElement("kbd", { className: "DocSearch-Commands-Key" }, /* @__PURE__ */ En.createElement(CommandIcon, { ariaLabel: selectKeyAriaLabel }, /* @__PURE__ */ En.createElement("polyline", { points: "9 10 4 15 9 20" }), /* @__PURE__ */ En.createElement("path", { d: "M20 4v7a4 4 0 0 1-4 4H4" }))), /* @__PURE__ */ En.createElement("span", { className: "DocSearch-Label" }, isAskAiActive ? submitQuestionText : selectText)), /* @__PURE__ */ En.createElement("li", null, /* @__PURE__ */ En.createElement("kbd", { className: "DocSearch-Commands-Key" }, /* @__PURE__ */ En.createElement("span", { className: "DocSearch-Escape-Key" }, "ESC")), /* @__PURE__ */ En.createElement("span", {
12957
+ const hasFooterAction = footerAction !== null && footerAction !== void 0 && typeof footerAction !== "boolean";
12958
+ return /* @__PURE__ */ En.createElement(En.Fragment, null, /* @__PURE__ */ En.createElement("ul", { className: "DocSearch-Commands" }, /* @__PURE__ */ En.createElement("li", null, /* @__PURE__ */ En.createElement("kbd", { className: "DocSearch-Commands-Key" }, /* @__PURE__ */ En.createElement(CommandIcon, { ariaLabel: navigateDownKeyAriaLabel }, /* @__PURE__ */ En.createElement("path", { d: "M12 5v14" }), /* @__PURE__ */ En.createElement("path", { d: "m19 12-7 7-7-7" }))), /* @__PURE__ */ En.createElement("kbd", { className: "DocSearch-Commands-Key" }, /* @__PURE__ */ En.createElement(CommandIcon, { ariaLabel: navigateUpKeyAriaLabel }, /* @__PURE__ */ En.createElement("path", { d: "m5 12 7-7 7 7" }), /* @__PURE__ */ En.createElement("path", { d: "M12 19V5" }))), /* @__PURE__ */ En.createElement("span", { className: "DocSearch-Label" }, navigateText)), /* @__PURE__ */ En.createElement("li", null, /* @__PURE__ */ En.createElement("kbd", { className: "DocSearch-Commands-Key" }, /* @__PURE__ */ En.createElement(CommandIcon, { ariaLabel: selectKeyAriaLabel }, /* @__PURE__ */ En.createElement("polyline", { points: "9 10 4 15 9 20" }), /* @__PURE__ */ En.createElement("path", { d: "M20 4v7a4 4 0 0 1-4 4H4" }))), /* @__PURE__ */ En.createElement("span", { className: "DocSearch-Label" }, isAskAiActive ? submitQuestionText : selectText)), /* @__PURE__ */ En.createElement("li", null, /* @__PURE__ */ En.createElement("kbd", { className: "DocSearch-Commands-Key" }, /* @__PURE__ */ En.createElement("span", { className: "DocSearch-Escape-Key" }, "ESC")), /* @__PURE__ */ En.createElement("span", {
12958
12959
  className: "DocSearch-Label",
12959
12960
  "aria-label": closeKeyAriaLabel
12960
- }, isAskAiActive ? backToSearchText : closeText))));
12961
+ }, isAskAiActive ? backToSearchText : closeText))), /* @__PURE__ */ En.createElement("div", { className: "DocSearch-Footer-Actions" }, hasFooterAction && /* @__PURE__ */ En.createElement("div", { className: "DocSearch-Footer-Action" }, footerAction), /* @__PURE__ */ En.createElement("div", { className: "DocSearch-Logo" }, /* @__PURE__ */ En.createElement(AlgoliaLogo, { translations: { poweredByText } }))));
12961
12962
  }
12962
12963
  function Hit({ hit, children }) {
12963
12964
  return /* @__PURE__ */ En.createElement("a", { href: hit.url }, children);
@@ -13115,7 +13116,7 @@ function useTrapFocus({ container }) {
13115
13116
  };
13116
13117
  }, [container]);
13117
13118
  }
13118
- function useDocSearchModalEffects({ initialScrollY, modalRef, snippetLength, theme }) {
13119
+ function useDocSearchModalEffects({ initialScrollY, modalRef, theme }) {
13119
13120
  useTheme({ theme });
13120
13121
  En.useEffect(() => {
13121
13122
  document.body.classList.add("DocSearch--active");
@@ -13135,9 +13136,6 @@ function useDocSearchModalEffects({ initialScrollY, modalRef, snippetLength, the
13135
13136
  document.body.style.marginInlineEnd = "0px";
13136
13137
  };
13137
13138
  }, []);
13138
- En.useEffect(() => {
13139
- if (window.matchMedia("(max-width: 768px)").matches) snippetLength.current = 5;
13140
- }, [snippetLength]);
13141
13139
  En.useEffect(() => {
13142
13140
  function setFullViewportHeight() {
13143
13141
  if (modalRef.current) {
@@ -13152,7 +13150,7 @@ function useDocSearchModalEffects({ initialScrollY, modalRef, snippetLength, the
13152
13150
  };
13153
13151
  }, [modalRef]);
13154
13152
  }
13155
- function useModalEnvironment({ getEnvironmentProps, containerRef, dropdownRef, formElementRef, inputRef, initialScrollY, modalRef, snippetLength, theme }) {
13153
+ function useModalEnvironment({ getEnvironmentProps, containerRef, dropdownRef, formElementRef, inputRef, initialScrollY, modalRef, theme }) {
13156
13154
  useTouchEvents({
13157
13155
  getEnvironmentProps,
13158
13156
  panelElement: dropdownRef.current,
@@ -13163,7 +13161,6 @@ function useModalEnvironment({ getEnvironmentProps, containerRef, dropdownRef, f
13163
13161
  useDocSearchModalEffects({
13164
13162
  initialScrollY,
13165
13163
  modalRef,
13166
- snippetLength,
13167
13164
  theme
13168
13165
  });
13169
13166
  }
@@ -13173,8 +13170,7 @@ function useModalRefs() {
13173
13170
  modalRef: En.useRef(null),
13174
13171
  formElementRef: En.useRef(null),
13175
13172
  dropdownRef: En.useRef(null),
13176
- inputRef: En.useRef(null),
13177
- snippetLength: En.useRef(15)
13173
+ inputRef: En.useRef(null)
13178
13174
  };
13179
13175
  }
13180
13176
  function useRefreshOnInitialQuery({ initialQuery, inputRef, refresh }) {
@@ -13528,7 +13524,7 @@ function buildNoQuerySources({ recentSearches, favoriteSearches, saveRecentSearc
13528
13524
  }
13529
13525
  }];
13530
13526
  }
13531
- async function buildQuerySources({ query, state: sourcesState, setContext, setStatus, searchClient, indexes, snippetLength, insights, appId, apiKey, maxResultsPerGroup, transformItems = identity, saveRecentSearch, onClose, facetSelections }) {
13527
+ async function buildQuerySources({ query, state: sourcesState, setContext, setStatus, searchClient, indexes, insights, appId, apiKey, maxResultsPerGroup, transformItems = identity, saveRecentSearch, onClose, facetSelections }) {
13532
13528
  const insightsActive = insights;
13533
13529
  try {
13534
13530
  const { results } = await searchClient.search({ requests: indexes.map((index) => {
@@ -13552,13 +13548,13 @@ async function buildQuerySources({ query, state: sourcesState, setContext, setSt
13552
13548
  "url"
13553
13549
  ],
13554
13550
  attributesToSnippet: (_searchParams$attribu2 = searchParams === null || searchParams === void 0 ? void 0 : searchParams.attributesToSnippet) !== null && _searchParams$attribu2 !== void 0 ? _searchParams$attribu2 : [
13555
- `hierarchy.lvl1:${snippetLength.current}`,
13556
- `hierarchy.lvl2:${snippetLength.current}`,
13557
- `hierarchy.lvl3:${snippetLength.current}`,
13558
- `hierarchy.lvl4:${snippetLength.current}`,
13559
- `hierarchy.lvl5:${snippetLength.current}`,
13560
- `hierarchy.lvl6:${snippetLength.current}`,
13561
- `content:${snippetLength.current}`
13551
+ `hierarchy.lvl1:15`,
13552
+ `hierarchy.lvl2:15`,
13553
+ `hierarchy.lvl3:15`,
13554
+ `hierarchy.lvl4:15`,
13555
+ `hierarchy.lvl5:15`,
13556
+ `hierarchy.lvl6:15`,
13557
+ `content:15`
13562
13558
  ],
13563
13559
  snippetEllipsisText: (_searchParams$snippet = searchParams === null || searchParams === void 0 ? void 0 : searchParams.snippetEllipsisText) !== null && _searchParams$snippet !== void 0 ? _searchParams$snippet : "…",
13564
13560
  highlightPreTag: (_searchParams$highlig = searchParams === null || searchParams === void 0 ? void 0 : searchParams.highlightPreTag) !== null && _searchParams$highlig !== void 0 ? _searchParams$highlig : "<mark>",
@@ -27408,7 +27404,7 @@ function useChat(_ref2 = {}) {
27408
27404
  }
27409
27405
 
27410
27406
  //#endregion
27411
- //#region ../docsearch-react/dist/esm/useSuggestedQuestions-Ds7H1JCr.js
27407
+ //#region ../docsearch-react/dist/esm/useSuggestedQuestions-DwcUPOf_.js
27412
27408
  init_compat();
27413
27409
  function AlertIcon(props) {
27414
27410
  return /* @__PURE__ */ En.createElement("svg", _objectSpread2$2({
@@ -27864,19 +27860,52 @@ function SourcesPanel({ links, titleText = "Source", pluralTitleText = "Sources"
27864
27860
  rel: "noopener noreferrer"
27865
27861
  }, /* @__PURE__ */ En.createElement(ContentIcon, null), /* @__PURE__ */ En.createElement("span", { className: "DocSearch-AskAiScreen-Sources-source-title" }, l.title || l.url)))))));
27866
27862
  }
27867
- /**
27868
- * Escapes HTML special characters to prevent XSS attacks. This should be used
27869
- * for any user-provided content that will be displayed.
27870
- */
27871
- function escapeHtml$1(unsafe) {
27863
+ /** Escapes HTML special characters for safe interpolation into HTML strings. */
27864
+ function escapeHtml(unsafe) {
27872
27865
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
27873
27866
  }
27867
+ /** Strips HTML tags and escapes the remainder for plain-text React children. */
27868
+ function sanitizeUserInput(input) {
27869
+ return escapeHtml(input.replace(/<[^>]*>/g, ""));
27870
+ }
27871
+ function decodeUrlForSchemeCheck(value) {
27872
+ let current = value;
27873
+ for (let i = 0; i < 3; i += 1) try {
27874
+ const decoded = decodeURIComponent(current);
27875
+ if (decoded === current) break;
27876
+ current = decoded;
27877
+ } catch (_unused) {
27878
+ break;
27879
+ }
27880
+ return current;
27881
+ }
27882
+ function stripControlsAndWhitespace(value) {
27883
+ let result = "";
27884
+ for (let i = 0; i < value.length; i += 1) {
27885
+ const code = value.charCodeAt(i);
27886
+ if (code > 32 && code !== 127) result += value[i];
27887
+ }
27888
+ return result;
27889
+ }
27874
27890
  /**
27875
- * Sanitizes user input to prevent XSS attacks. Removes any HTML tags and
27876
- * escapes special characters.
27891
+ * Returns a URL safe for href/src, or '' if unsafe. Does not HTML-escape
27892
+ * callers building HTML strings must escape separately.
27877
27893
  */
27878
- function sanitizeUserInput(input) {
27879
- return escapeHtml$1(input.replace(/<[^>]*>/g, ""));
27894
+ function sanitizeUrl(url) {
27895
+ if (!url) return "";
27896
+ const trimmed = url.trim();
27897
+ if (!trimmed) return "";
27898
+ const normalized = stripControlsAndWhitespace(decodeUrlForSchemeCheck(trimmed));
27899
+ if (!normalized) return "";
27900
+ if (normalized.startsWith("//")) return "";
27901
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(normalized)) return trimmed;
27902
+ try {
27903
+ const parsed = new URL(normalized);
27904
+ if (parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "mailto:") return normalized;
27905
+ } catch (_unused2) {
27906
+ return "";
27907
+ }
27908
+ return "";
27880
27909
  }
27881
27910
  function extractLinksFromMessage(message) {
27882
27911
  const links = [];
@@ -27891,8 +27920,8 @@ function extractLinksFromMessage(message) {
27891
27920
  const markdownMatches = cleanText.matchAll(markdownLinkRegex);
27892
27921
  for (const match of markdownMatches) {
27893
27922
  const title = match[1].trim();
27894
- const url = match[2];
27895
- if (!seen.has(url)) {
27923
+ const url = sanitizeUrl(match[2]);
27924
+ if (url && !seen.has(url)) {
27896
27925
  seen.add(url);
27897
27926
  links.push({
27898
27927
  url,
@@ -27902,8 +27931,8 @@ function extractLinksFromMessage(message) {
27902
27931
  }
27903
27932
  const plainUrls = cleanText.matchAll(plainLinkRegex);
27904
27933
  for (const match of plainUrls) {
27905
- const cleanUrl = match[0].replace(/[.,;:!?]+$/, "");
27906
- if (!seen.has(cleanUrl)) {
27934
+ const cleanUrl = sanitizeUrl(match[0].replace(/[.,;:!?]+$/, ""));
27935
+ if (cleanUrl && !seen.has(cleanUrl)) {
27907
27936
  seen.add(cleanUrl);
27908
27937
  links.push({ url: cleanUrl });
27909
27938
  }
@@ -27939,11 +27968,11 @@ const getMessageContent = (message) => {
27939
27968
  var _message$parts;
27940
27969
  return ((_message$parts = message === null || message === void 0 ? void 0 : message.parts) !== null && _message$parts !== void 0 ? _message$parts : []).filter((part) => part.type === "text").map((part) => part.text).join("\n\n");
27941
27970
  };
27942
- /** Helper function to check if error is a thread depth error (AI-217). */
27971
+ /** Helper function to check if an error reports the conversation depth limit. */
27943
27972
  function isThreadDepthError(error) {
27944
27973
  var _error$message;
27945
27974
  if (!error) return false;
27946
- return ((_error$message = error.message) === null || _error$message === void 0 ? void 0 : _error$message.includes("AI-217")) || false;
27975
+ return /(?:ai-217|conversation\s+depth)/i.test((_error$message = error.message) !== null && _error$message !== void 0 ? _error$message : "");
27947
27976
  }
27948
27977
  const EMPTY_TOOLS = Object.freeze({});
27949
27978
  function isAIToolPart(part) {
@@ -28091,12 +28120,10 @@ function ToolCall({ part, translations, tools, onSearchQueryClick, memoryEnabled
28091
28120
  });
28092
28121
  return null;
28093
28122
  }
28094
- function escapeHtml(html) {
28095
- return html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
28096
- }
28097
28123
  const renderer = new k.Renderer();
28098
28124
  renderer.code = ({ text, lang = "", escaped }) => {
28099
- const languageClass = lang ? `language-${lang}` : "";
28125
+ const safeLang = /^[a-zA-Z0-9_-]+$/.test(lang) ? lang : "";
28126
+ const languageClass = safeLang ? `language-${safeLang}` : "";
28100
28127
  const safeCode = escaped ? text : escapeHtml(text);
28101
28128
  return `
28102
28129
  <div class="DocSearch-CodeSnippet">
@@ -28106,15 +28133,28 @@ renderer.code = ({ text, lang = "", escaped }) => {
28106
28133
  `;
28107
28134
  };
28108
28135
  renderer.link = ({ href, title, text }) => {
28136
+ const safeHref = escapeHtml(sanitizeUrl(href));
28137
+ const textEscaped = escapeHtml(text);
28138
+ if (!safeHref) return textEscaped;
28139
+ return `<a href="${safeHref}"${title ? ` title="${escapeHtml(title)}"` : ""} target="_blank" rel="noopener noreferrer">${textEscaped}</a>`;
28140
+ };
28141
+ renderer.image = ({ href, title, text }) => {
28142
+ const safeHref = escapeHtml(sanitizeUrl(href));
28143
+ if (!safeHref) return escapeHtml(text);
28109
28144
  const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
28110
- return `<a href="${href ? escapeHtml(href) : ""}"${titleAttr} target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>`;
28145
+ return `<img src="${safeHref}" alt="${escapeHtml(text)}"${titleAttr} />`;
28111
28146
  };
28112
- const MemoizedMarkdown = D$1(({ content, copyButtonText, copyButtonCopiedText, isStreaming }) => {
28113
- const html = q$2(() => k.parse(content, {
28147
+ renderer.html = ({ text }) => escapeHtml(text);
28148
+ /** Parses markdown into HTML safe for `dangerouslySetInnerHTML`. */
28149
+ function parseMarkdownToSafeHtml(content) {
28150
+ return k.parse(content, {
28114
28151
  gfm: true,
28115
28152
  breaks: true,
28116
28153
  renderer
28117
- }), [content]);
28154
+ });
28155
+ }
28156
+ const MemoizedMarkdown = D$1(({ content, copyButtonText, copyButtonCopiedText, isStreaming }) => {
28157
+ const html = q$2(() => parseMarkdownToSafeHtml(content), [content]);
28118
28158
  const containerRef = F$2(null);
28119
28159
  _$1(() => {
28120
28160
  const container = containerRef.current;
@@ -31268,7 +31308,7 @@ function createAutocomplete(options) {
31268
31308
  }
31269
31309
 
31270
31310
  //#endregion
31271
- //#region ../docsearch-react/dist/esm/DocSearchAskAiModal-BczGFkvR.js
31311
+ //#region ../docsearch-react/dist/esm/DocSearchAskAiModal-C_sH1j8Y.js
31272
31312
  init_compat();
31273
31313
  function MessageIcon() {
31274
31314
  return /* @__PURE__ */ En.createElement("svg", {
@@ -31813,7 +31853,8 @@ const _excluded$7 = [
31813
31853
  "recentSearchesWithFavoritesLimit",
31814
31854
  "indices",
31815
31855
  "facets",
31816
- "isHybridModeSupported"
31856
+ "isHybridModeSupported",
31857
+ "footerAction"
31817
31858
  ];
31818
31859
  const _excluded2 = [
31819
31860
  "footer",
@@ -31822,7 +31863,7 @@ const _excluded2 = [
31822
31863
  ];
31823
31864
  function DocSearchAskAiModal(_ref) {
31824
31865
  var _translations$searchB, _askAiConfig$memory$e, _askAiConfig$memory, _askAiConfig$tools;
31825
- let { appId, apiKey, askAi, maxResultsPerGroup, theme, onClose = noop$2, transformItems = identity, hitComponent = Hit, resultsFooterComponent = () => null, navigator, initialScrollY = 0, transformSearchClient = identity, disableUserPersonalization = false, initialQuery: initialQueryFromProp = "", translations = {}, getMissingResultsUrl, insights = false, onAskAiToggle, interceptAskAiEvent, isAskAiActive = false, recentSearchesLimit = 7, recentSearchesWithFavoritesLimit = 4, indices, facets, isHybridModeSupported = false } = _ref, props = _objectWithoutProperties$6(_ref, _excluded$7);
31866
+ let { appId, apiKey, askAi, maxResultsPerGroup, theme, onClose = noop$2, transformItems = identity, hitComponent = Hit, resultsFooterComponent = () => null, navigator, initialScrollY = 0, transformSearchClient = identity, disableUserPersonalization = false, initialQuery: initialQueryFromProp = "", translations = {}, getMissingResultsUrl, insights = false, onAskAiToggle, interceptAskAiEvent, isAskAiActive = false, recentSearchesLimit = 7, recentSearchesWithFavoritesLimit = 4, indices, facets, isHybridModeSupported = false, footerAction } = _ref, props = _objectWithoutProperties$6(_ref, _excluded$7);
31826
31867
  const { footer: footerTranslations, searchBox: searchBoxTranslations, facets: facetBarTranslations } = translations, screenStateTranslations = _objectWithoutProperties$6(translations, _excluded2);
31827
31868
  const [state, setState] = En.useState({
31828
31869
  query: "",
@@ -31843,7 +31884,7 @@ function DocSearchAskAiModal(_ref) {
31843
31884
  var _translations$searchB3;
31844
31885
  placeholder = (translations === null || translations === void 0 || (_translations$searchB3 = translations.searchBox) === null || _translations$searchB3 === void 0 ? void 0 : _translations$searchB3.placeholderTextAskAi) || "Ask another question...";
31845
31886
  }
31846
- const { containerRef, modalRef, formElementRef, dropdownRef, inputRef, snippetLength } = useModalRefs();
31887
+ const { containerRef, modalRef, formElementRef, dropdownRef, inputRef } = useModalRefs();
31847
31888
  const { initialQuery, initialQueryFromSelection } = useInitialModalQuery(initialQueryFromProp);
31848
31889
  const searchClient = useSearchClient(appId, apiKey, transformSearchClient);
31849
31890
  const askAiConfig = typeof askAi === "object" ? askAi : null;
@@ -31997,7 +32038,6 @@ function DocSearchAskAiModal(_ref) {
31997
32038
  setStatus,
31998
32039
  searchClient,
31999
32040
  indexes,
32000
- snippetLength,
32001
32041
  insights: Boolean(insights),
32002
32042
  appId,
32003
32043
  apiKey,
@@ -32027,7 +32067,6 @@ function DocSearchAskAiModal(_ref) {
32027
32067
  inputRef,
32028
32068
  initialScrollY,
32029
32069
  modalRef,
32030
- snippetLength,
32031
32070
  theme
32032
32071
  });
32033
32072
  En.useEffect(() => {
@@ -32166,7 +32205,8 @@ function DocSearchAskAiModal(_ref) {
32166
32205
  })),
32167
32206
  footer: /* @__PURE__ */ En.createElement(Footer, {
32168
32207
  translations: footerTranslations,
32169
- isAskAiActive
32208
+ isAskAiActive,
32209
+ footerAction
32170
32210
  }),
32171
32211
  onClose
32172
32212
  });
@@ -32251,7 +32291,7 @@ init_compat();
32251
32291
  function isEditingContent(event) {
32252
32292
  const element = event.composedPath()[0];
32253
32293
  const tagName = element.tagName;
32254
- return element.isContentEditable || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA";
32294
+ return element.isContentEditable || tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA" || tagName === "BUTTON";
32255
32295
  }
32256
32296
  function useDocSearchKeyboardEvents({ isOpen, isAskAiActive, onAskAiToggle, onClose, onOpen, keyboardShortcuts = DEFAULT_KEYBOARD_SHORTCUTS }) {
32257
32297
  En.useEffect(() => {
@@ -32549,7 +32589,8 @@ const _excluded = [
32549
32589
  "environment",
32550
32590
  "transformSearchClient",
32551
32591
  "hitComponent",
32552
- "resultsFooterComponent"
32592
+ "resultsFooterComponent",
32593
+ "footerAction"
32553
32594
  ];
32554
32595
  function getHTMLElement(value, env) {
32555
32596
  if (typeof value !== "string") return value;
@@ -32572,14 +32613,16 @@ function createTemplateFunction(original) {
32572
32613
  function createDocSearch(Component, version) {
32573
32614
  return (allProps) => {
32574
32615
  const input = allProps;
32575
- const { container, environment, transformSearchClient, hitComponent, resultsFooterComponent } = input, rest = _objectWithoutProperties$5(input, _excluded);
32616
+ const { container, environment, transformSearchClient, hitComponent, resultsFooterComponent, footerAction } = input, rest = _objectWithoutProperties$5(input, _excluded);
32576
32617
  const containerElement = getHTMLElement(container, environment || (typeof window !== "undefined" ? window : void 0));
32577
32618
  const ref = b$2();
32578
32619
  let isReady = false;
32620
+ const FooterAction = createTemplateFunction(footerAction);
32579
32621
  J$1(_$2(Component, _objectSpread2$1(_objectSpread2$1({}, rest), {}, {
32580
32622
  ref,
32581
32623
  hitComponent: createTemplateFunction(hitComponent),
32582
32624
  resultsFooterComponent: createTemplateFunction(resultsFooterComponent),
32625
+ footerAction: FooterAction ? _$2(FooterAction, null) : void 0,
32583
32626
  transformSearchClient: (searchClient) => {
32584
32627
  if (typeof searchClient === "object" && searchClient !== null && "addAlgoliaAgent" in searchClient && typeof searchClient.addAlgoliaAgent === "function") searchClient.addAlgoliaAgent("docsearch.js", version);
32585
32628
  return typeof transformSearchClient === "function" ? transformSearchClient(searchClient) : searchClient;