Skip to content

Commit f0be8e7

Browse files
ly-wang19claudevincentkoc
authored
fix(duckduckgo): decode & last in decodeHtmlEntities to avoid double-decoding (#96348)
* fix(duckduckgo): decode &amp; last in decodeHtmlEntities to avoid double-decoding decodeHtmlEntities decoded &amp; FIRST, so result text that literally contains an entity (e.g. a page title 'How to escape &lt; in HTML', which DuckDuckGo returns double-encoded as '&amp;lt;') was re-decoded into markup: '&amp;lt;' became '<' instead of the literal '&lt;', corrupting the titles, snippets, and URLs the web-search tool returns to the model. Reorder so &amp; is decoded last, matching the established convention elsewhere in the codebase (msteams/inbound.ts, openai-transport-stream.ts, launchd-plist.ts, doctor-session-snapshots.ts all decode &amp; last). Behavior-preserving for all singly-encoded input. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(duckduckgo): decode html entities in one pass --------- Co-authored-by: ly-wang19 <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
1 parent 80bd000 commit f0be8e7

2 files changed

Lines changed: 54 additions & 15 deletions

File tree

extensions/duckduckgo/src/ddg-client.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,49 @@ type DuckDuckGoResult = {
3636
};
3737

3838
function decodeHtmlEntities(text: string): string {
39-
return text
40-
.replace(/&amp;/g, "&")
41-
.replace(/&lt;/g, "<")
42-
.replace(/&gt;/g, ">")
43-
.replace(/&quot;/g, '"')
44-
.replace(/&apos;/g, "'")
45-
.replace(/&#39;/g, "'")
46-
.replace(/&#x27;/g, "'")
47-
.replace(/&#x2F;/g, "/")
48-
.replace(/&nbsp;/g, " ")
49-
.replace(/&ndash;/g, "-")
50-
.replace(/&mdash;/g, "--")
51-
.replace(/&hellip;/g, "...")
52-
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
53-
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16)));
39+
return text.replace(
40+
/&(?:lt|gt|quot|apos|#39|#x27|#x2F|nbsp|ndash|mdash|hellip|amp|#\d+|#x[0-9a-f]+);/gi,
41+
(entity) => {
42+
const normalized = entity.toLowerCase();
43+
if (normalized === "&lt;") {
44+
return "<";
45+
}
46+
if (normalized === "&gt;") {
47+
return ">";
48+
}
49+
if (normalized === "&quot;") {
50+
return '"';
51+
}
52+
if (normalized === "&apos;" || normalized === "&#39;" || normalized === "&#x27;") {
53+
return "'";
54+
}
55+
if (normalized === "&#x2f;") {
56+
return "/";
57+
}
58+
if (normalized === "&nbsp;") {
59+
return " ";
60+
}
61+
if (normalized === "&ndash;") {
62+
return "-";
63+
}
64+
if (normalized === "&mdash;") {
65+
return "--";
66+
}
67+
if (normalized === "&hellip;") {
68+
return "...";
69+
}
70+
if (normalized === "&amp;") {
71+
return "&";
72+
}
73+
if (normalized.startsWith("&#x")) {
74+
return String.fromCodePoint(Number.parseInt(normalized.slice(3, -1), 16));
75+
}
76+
if (normalized.startsWith("&#")) {
77+
return String.fromCodePoint(Number.parseInt(normalized.slice(2, -1), 10));
78+
}
79+
return entity;
80+
},
81+
);
5482
}
5583

5684
function stripHtml(html: string): string {

extensions/duckduckgo/src/ddg-search-provider.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,17 @@ describe("duckduckgo web search provider", () => {
186186
);
187187
});
188188

189+
it("does not double-decode escaped entities (decodes &amp; last)", () => {
190+
// A result whose text literally shows "&lt;" arrives double-encoded as
191+
// "&amp;lt;". Decoding &amp; first would re-decode it into "<", corrupting
192+
// the snippet; &amp; must be decoded last.
193+
expect(ddgClientTesting.decodeHtmlEntities("How to escape &amp;lt; in HTML")).toBe(
194+
"How to escape &lt; in HTML",
195+
);
196+
expect(ddgClientTesting.decodeHtmlEntities("a&amp;#39;b")).toBe("a&#39;b");
197+
expect(ddgClientTesting.decodeHtmlEntities("a&#x26;amp;b")).toBe("a&amp;b");
198+
});
199+
189200
it("parses results when href appears before class", () => {
190201
const html = `
191202
<a href="https://duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com" class="result__a">

0 commit comments

Comments
 (0)