@salesforcedevs/docs-components 1.32.0 → 1.33.1
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.
|
|
3
|
+
"version": "1.33.1",
|
|
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": "
|
|
28
|
+
"gitHead": "71d75798db2bfb52ea29b57f52947190aa0ec092"
|
|
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 `<`/`>`/`&` 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
|
+
* `"` 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
|
+
}
|