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.
- package/ai/google_search_ai.js +68 -3
- package/package.json +1 -1
- package/utils/html-to-markdown.js +51 -48
package/ai/google_search_ai.js
CHANGED
|
@@ -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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
@@ -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-
|
|
153
|
-
//
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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);
|