diff --git a/extensions/duckduckgo/src/ddg-client.ts b/extensions/duckduckgo/src/ddg-client.ts index bcecbc7b4b0a..b8cd6ba3a90b 100644 --- a/extensions/duckduckgo/src/ddg-client.ts +++ b/extensions/duckduckgo/src/ddg-client.ts @@ -36,21 +36,49 @@ type DuckDuckGoResult = { }; function decodeHtmlEntities(text: string): string { - return text - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/'/g, "'") - .replace(/'/g, "'") - .replace(///g, "/") - .replace(/ /g, " ") - .replace(/–/g, "-") - .replace(/—/g, "--") - .replace(/…/g, "...") - .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))) - .replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16))); + return text.replace( + /&(?:lt|gt|quot|apos|#39|#x27|#x2F|nbsp|ndash|mdash|hellip|amp|#\d+|#x[0-9a-f]+);/gi, + (entity) => { + const normalized = entity.toLowerCase(); + if (normalized === "<") { + return "<"; + } + if (normalized === ">") { + return ">"; + } + if (normalized === """) { + return '"'; + } + if (normalized === "'" || normalized === "'" || normalized === "'") { + return "'"; + } + if (normalized === "/") { + return "/"; + } + if (normalized === " ") { + return " "; + } + if (normalized === "–") { + return "-"; + } + if (normalized === "—") { + return "--"; + } + if (normalized === "…") { + return "..."; + } + if (normalized === "&") { + return "&"; + } + if (normalized.startsWith("&#x")) { + return String.fromCodePoint(Number.parseInt(normalized.slice(3, -1), 16)); + } + if (normalized.startsWith("&#")) { + return String.fromCodePoint(Number.parseInt(normalized.slice(2, -1), 10)); + } + return entity; + }, + ); } function stripHtml(html: string): string { diff --git a/extensions/duckduckgo/src/ddg-search-provider.test.ts b/extensions/duckduckgo/src/ddg-search-provider.test.ts index 2728f3ca9a43..b86f48e5fc14 100644 --- a/extensions/duckduckgo/src/ddg-search-provider.test.ts +++ b/extensions/duckduckgo/src/ddg-search-provider.test.ts @@ -186,6 +186,17 @@ describe("duckduckgo web search provider", () => { ); }); + it("does not double-decode escaped entities (decodes & last)", () => { + // A result whose text literally shows "<" arrives double-encoded as + // "&lt;". Decoding & first would re-decode it into "<", corrupting + // the snippet; & must be decoded last. + expect(ddgClientTesting.decodeHtmlEntities("How to escape &lt; in HTML")).toBe( + "How to escape < in HTML", + ); + expect(ddgClientTesting.decodeHtmlEntities("a&#39;b")).toBe("a'b"); + expect(ddgClientTesting.decodeHtmlEntities("a&amp;b")).toBe("a&b"); + }); + it("parses results when href appears before class", () => { const html = `