decant-core 1.2.2 → 1.2.4

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,70 @@
1
1
  import { ChatParser } from "./base.js";
2
2
  import { convertToMarkdown } from "../utils/html-to-markdown.js";
3
3
 
4
+ export function sanitizeResponseContainer(container) {
5
+ if (!container) return container;
6
+ const clone =
7
+ typeof container.cloneNode === "function"
8
+ ? container.cloneNode(true)
9
+ : container;
10
+
11
+ // 1. Remove script, style, and noscript elements to prevent inline script leakage
12
+ const unwanted = clone.querySelectorAll
13
+ ? clone.querySelectorAll("script, style, noscript")
14
+ : [];
15
+ unwanted.forEach((el) => el.remove());
16
+
17
+ // 2. Remove base64 inline images to prevent megabyte-scale text walls in markdown exports
18
+ const images = clone.querySelectorAll ? clone.querySelectorAll("img") : [];
19
+ images.forEach((img) => {
20
+ const src = img.getAttribute("src") || "";
21
+ if (src.startsWith("data:image/")) {
22
+ img.remove();
23
+ }
24
+ });
25
+
26
+ // 3. Unwrap Google tracking redirects (/goto?url=..., /url?q=...) to direct URLs
27
+ const links = clone.querySelectorAll ? clone.querySelectorAll("a[href]") : [];
28
+ links.forEach((a) => {
29
+ const href = a.getAttribute("href") || "";
30
+ if (
31
+ href.startsWith("/goto?") ||
32
+ href.startsWith("/url?") ||
33
+ href.includes("google.com/url?") ||
34
+ href.includes("google.com/goto?")
35
+ ) {
36
+ try {
37
+ const parsed = new URL(href, "https://www.google.com");
38
+ const target =
39
+ parsed.searchParams.get("url") || parsed.searchParams.get("q");
40
+ if (
41
+ target &&
42
+ (target.startsWith("http://") || target.startsWith("https://"))
43
+ ) {
44
+ a.setAttribute("href", target);
45
+ }
46
+ } catch {
47
+ // Keep original href if URL parsing fails
48
+ }
49
+ }
50
+
51
+ // 4. Remove empty link shells left behind by stripped images/tracking icons
52
+ if (!a.textContent.trim() && !a.querySelector("img, svg")) {
53
+ a.remove();
54
+ }
55
+ });
56
+
57
+ return clone;
58
+ }
59
+
60
+ export function cleanMarkdownSpacing(markdown) {
61
+ if (!markdown) return "";
62
+ return markdown
63
+ .replace(/[ \t]+$/gm, "")
64
+ .replace(/\n{3,}/g, "\n\n")
65
+ .trim();
66
+ }
67
+
4
68
  export class GoogleSearchAIParser extends ChatParser {
5
69
  name = "Google Search AI";
6
70
  isAvailable(url) {
@@ -94,9 +158,10 @@ export class GoogleSearchAIParser extends ChatParser {
94
158
  const minLength = Math.min(queries.length, responseContainers.length);
95
159
  for (let i = 0; i < minLength; i++) {
96
160
  messages.push({ role: "User", content: queries[i].trim() });
97
- const text = convertToMarkdown(responseContainers[i]);
98
- if (text.trim()) {
99
- messages.push({ role: "Model", content: text.trim() });
161
+ const cleanContainer = sanitizeResponseContainer(responseContainers[i]);
162
+ const text = cleanMarkdownSpacing(convertToMarkdown(cleanContainer));
163
+ if (text) {
164
+ messages.push({ role: "Model", content: text });
100
165
  }
101
166
  }
102
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decant-core",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Shared AI chat platform parsers and detection for Covai browser extensions.",
5
5
  "type": "module",
6
6
  "main": "ai/index.js",
@@ -22,6 +22,21 @@ import { strikethrough, taskListItems } from "turndown-plugin-gfm";
22
22
  * @returns {string} Markdown formatted text
23
23
  */
24
24
  export function convertToMarkdown(htmlContent, options = {}) {
25
+ const mathPlaceholders = [];
26
+ let mathCounter = 0;
27
+
28
+ const registerMath = (element, latex, isBlock, doc) => {
29
+ if (!element || !element.parentNode) return;
30
+ const cleanLatex = (latex || "").trim();
31
+ if (!cleanLatex) return;
32
+ const token = "DECANTMATHPLACEHOLDER" + mathCounter++ + "X";
33
+ const text = isBlock
34
+ ? "\n\n$$" + cleanLatex + "$$\n\n"
35
+ : "$" + cleanLatex + "$";
36
+ mathPlaceholders.push({ token, text });
37
+ const textNode = doc.createTextNode(token);
38
+ element.parentNode.replaceChild(textNode, element);
39
+ };
25
40
  // Configure Turndown service
26
41
  const turndownService = new TurndownService({
27
42
  headingStyle: "atx", // Use # for headings (not underline style)
@@ -149,57 +164,20 @@ export function convertToMarkdown(htmlContent, options = {}) {
149
164
  // Clone the element to avoid modifying the original
150
165
  const clone = htmlContent.cloneNode(true);
151
166
 
152
- // Convert math equations (KaTeX and data-math) to standard markdown math blocks
153
- // 1. Process block display KaTeX
154
- clone.querySelectorAll(".katex-display").forEach((el) => {
155
- const annotation = el.querySelector(
156
- 'annotation[encoding="application/x-tex"]',
157
- );
158
- if (annotation) {
159
- const latex = annotation.textContent.trim();
160
- const textNode = clone.ownerDocument.createTextNode(
161
- `\n\n$$${latex}$$\n\n`,
162
- );
163
- el.parentNode.replaceChild(textNode, el);
164
- } else {
165
- const textNode = clone.ownerDocument.createTextNode(
166
- `\n\n$$${el.textContent.trim()}$$\n\n`,
167
- );
168
- el.parentNode.replaceChild(textNode, el);
169
- }
170
- });
171
-
172
- // 2. Process inline KaTeX
173
- clone.querySelectorAll(".katex").forEach((el) => {
174
- if (!el.parentNode) return;
175
- const annotation = el.querySelector(
176
- 'annotation[encoding="application/x-tex"]',
177
- );
178
- if (annotation) {
179
- const latex = annotation.textContent.trim();
180
- const textNode = clone.ownerDocument.createTextNode(`$${latex}$`);
181
- el.parentNode.replaceChild(textNode, el);
182
- } else {
183
- const textNode = clone.ownerDocument.createTextNode(
184
- `$${el.textContent.trim()}$`,
185
- );
186
- el.parentNode.replaceChild(textNode, el);
187
- }
188
- });
189
-
190
- // 3. Process Gemini-style data-math attributes
167
+ // Convert math equations (KaTeX, data-math, and data-xpm-latex) to standard markdown math blocks
168
+ // Use alphanumeric placeholder tokens so Turndown text-escaping engine never corrupts LaTeX
169
+ // 1. Process Gemini-style data-math attributes first (removes child .katex spans so they are not double-processed)
191
170
  clone.querySelectorAll("[data-math]").forEach((el) => {
192
- if (!el.parentNode) return;
171
+ if (!clone.contains(el)) return;
193
172
  const latex = el.getAttribute("data-math");
194
173
  const isBlock =
195
174
  el.classList.contains("math-block") || el.tagName === "DIV";
196
- const replacementText = isBlock ? `\n\n$$${latex}$$\n\n` : `$${latex}$`;
197
- const textNode = clone.ownerDocument.createTextNode(replacementText);
198
- el.parentNode.replaceChild(textNode, el);
175
+ registerMath(el, latex, isBlock, clone.ownerDocument);
199
176
  });
200
177
 
201
- // 4. Process Google Search SGE LaTeX images with [data-xpm-latex]
178
+ // 2. Process Google Search SGE LaTeX images with [data-xpm-latex]
202
179
  clone.querySelectorAll("[data-xpm-latex]").forEach((el) => {
180
+ if (!clone.contains(el)) return;
203
181
  const copyRoot = el.closest("[data-xpm-copy-root]");
204
182
  if (!copyRoot) return;
205
183
  const container = el.closest(".cPGBZb") || copyRoot;
@@ -217,9 +195,31 @@ export function convertToMarkdown(htmlContent, options = {}) {
217
195
  isBlock = parentText === "";
218
196
  }
219
197
 
220
- const replacementText = isBlock ? `\n\n$$${latex}$$\n\n` : `$${latex}$`;
221
- const textNode = clone.ownerDocument.createTextNode(replacementText);
222
- container.parentNode.replaceChild(textNode, container);
198
+ registerMath(container, latex, isBlock, clone.ownerDocument);
199
+ });
200
+
201
+ // 3. Process block display KaTeX (.katex-display)
202
+ clone.querySelectorAll(".katex-display").forEach((el) => {
203
+ if (!clone.contains(el)) return;
204
+ const annotation = el.querySelector(
205
+ 'annotation[encoding="application/x-tex"]',
206
+ );
207
+ const latex = annotation
208
+ ? annotation.textContent.trim()
209
+ : el.textContent.trim();
210
+ registerMath(el, latex, true, clone.ownerDocument);
211
+ });
212
+
213
+ // 4. Process inline KaTeX (.katex)
214
+ clone.querySelectorAll(".katex").forEach((el) => {
215
+ if (!clone.contains(el)) return;
216
+ const annotation = el.querySelector(
217
+ 'annotation[encoding="application/x-tex"]',
218
+ );
219
+ const latex = annotation
220
+ ? annotation.textContent.trim()
221
+ : el.textContent.trim();
222
+ registerMath(el, latex, false, clone.ownerDocument);
223
223
  });
224
224
 
225
225
  // Preprocess code blocks (pre elements) to normalize formatting and language tags
@@ -371,7 +371,10 @@ export function convertToMarkdown(htmlContent, options = {}) {
371
371
  }
372
372
 
373
373
  try {
374
- const markdown = turndownService.turndown(html);
374
+ let markdown = turndownService.turndown(html);
375
+ for (const { token, text } of mathPlaceholders) {
376
+ markdown = markdown.replace(token, () => text);
377
+ }
375
378
  return markdown.trim();
376
379
  } catch (error) {
377
380
  console.error("Error converting HTML to markdown:", error);