@salesforcedevs/docs-components 1.32.0 → 1.33.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/docs-components",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -25,5 +25,5 @@
25
25
  "@types/lodash.orderby": "4.6.9",
26
26
  "@types/lodash.uniqby": "4.7.9"
27
27
  },
28
- "gitHead": "c45be158e09722da65cded05e0877f02b9d8a37a"
28
+ "gitHead": "e85fc1bb496e99a4ba453db278a465079bcf6385"
29
29
  }
@@ -2,6 +2,9 @@
2
2
  // The ONLY place a /api/search URL is built, and the ONLY place the HTTP
3
3
  // contract lives. No LWC / arch / dx imports — browser DOM APIs only.
4
4
 
5
+ /** Where a result's `snippet` came from (mirrors the backend contract). */
6
+ export type SnippetSource = "headline" | "description" | "none";
7
+
5
8
  export interface SearchResult {
6
9
  url: string;
7
10
  title: string;
@@ -14,6 +17,12 @@ export interface SearchResult {
14
17
  lexScore: number;
15
18
  semScore: number;
16
19
  matchType: string;
20
+ // Contextual highlighted passage (P4.5). Always XSS-safe HTML from the
21
+ // backend, but only display-correct as HTML when snippetSource is
22
+ // "headline" (see highlightedSnippetHtml). Optional so pre-P4.5 backends
23
+ // and existing fixtures still satisfy the type.
24
+ snippet?: string | null;
25
+ snippetSource?: SnippetSource;
17
26
  }
18
27
 
19
28
  export interface FacetBucket {
@@ -158,3 +167,54 @@ export function decodeHtmlEntities(value: unknown): string {
158
167
  const doc = new DOMParser().parseFromString(value, "text/html");
159
168
  return doc.documentElement.textContent ?? "";
160
169
  }
170
+
171
+ /**
172
+ * Defense-in-depth sanitizer for a backend `snippet` (P4.5). The backend
173
+ * already emits XSS-safe HTML whose ONLY tags are the `<b>`/`</b>` it wrapped
174
+ * around matched terms (escape-then-mark via PUA sentinels), so this is a
175
+ * belt-and-braces second gate on the client: strip every tag except `<b>` and
176
+ * `</b>`. A malformed backend, a proxy that rewrites bodies, or a future
177
+ * contract drift can't inject anything else. Text between the bold tags is
178
+ * left byte-for-byte (it is already entity-escaped by the backend, so literal
179
+ * `<`/`>`/`&` in prose arrive as `&lt;`/`&gt;`/`&amp;` and stay inert).
180
+ */
181
+ export function sanitizeSnippetHtml(value: unknown): string {
182
+ // Strip every tag except a BARE <b> or </b> (case-insensitive). The backend
183
+ // only ever emits attribute-less bold tags, so requiring the `>` right after
184
+ // `b` also drops an attribute-bearing `<b onmouseover=…>` — the tightest
185
+ // possible allowlist. Text between tags is left as-is (already escaped).
186
+ //
187
+ // The trailing `(?:>|$)` also consumes an UNTERMINATED tag that runs to the
188
+ // end of the string (e.g. `…<img src=x onerror=alert(1)` with no closing
189
+ // `>`). A plain `…>` alternative would leave that fragment as literal text;
190
+ // today's sole sink (innerHTML at end-of-fragment) happens to drop such a
191
+ // tag, but this helper is exported and must honor its own contract without
192
+ // leaning on the parser's incidental EOF behavior.
193
+ return String(value ?? "").replace(/<(?!\/?b>)[^>]*(?:>|$)/gi, "");
194
+ }
195
+
196
+ /**
197
+ * The HTML to render for a result's highlighted snippet, or `null` when the
198
+ * caller should fall back to the plain-text description path.
199
+ *
200
+ * Only `snippetSource === "headline"` yields HTML: that branch is
201
+ * `markHtml(headline)` over `chunk_text`, which the backend already DECODED
202
+ * (entities resolved, markup stripped) and then escaped, so it renders
203
+ * display-correct as HTML. The `"description"` branch, by contrast, is
204
+ * `escapeHtml(rawDescription)` where the raw description is still
205
+ * entity-ENCODED frontmatter (verified on the live corpus: descriptions carry
206
+ * `&#x22;` etc.) — rendering that as HTML would double-escape and show literal
207
+ * entities, a regression vs. the existing `decodeHtmlEntities(description)`
208
+ * text path. So for anything but a headline we return `null` and let the
209
+ * caller keep today's text rendering.
210
+ */
211
+ export function highlightedSnippetHtml(result: SearchResult): string | null {
212
+ if (result.snippetSource !== "headline") {
213
+ return null;
214
+ }
215
+ const snippet = result.snippet;
216
+ if (typeof snippet !== "string" || snippet === "") {
217
+ return null;
218
+ }
219
+ return sanitizeSnippetHtml(snippet);
220
+ }