decant-core 1.2.1 → 1.2.3
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/gemini.js +31 -2
- package/package.json +1 -1
- package/utils/html-to-markdown.js +51 -48
package/ai/gemini.js
CHANGED
|
@@ -4,6 +4,16 @@ import { convertToMarkdown } from "../utils/html-to-markdown.js";
|
|
|
4
4
|
const GEMINI_RPC_ID = "hNvQHb";
|
|
5
5
|
const DEFAULT_BARD_PATH = "/_/BardChatUi";
|
|
6
6
|
|
|
7
|
+
function isValidMessageText(str, convoId = "") {
|
|
8
|
+
if (typeof str !== "string") return false;
|
|
9
|
+
const trimmed = str.trim();
|
|
10
|
+
if (!trimmed) return false;
|
|
11
|
+
if (/^(?:c_|rc_|r_)[a-zA-Z0-9_-]+$/.test(trimmed)) return false;
|
|
12
|
+
if (convoId && (trimmed === convoId || trimmed === `c_${convoId}`))
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
export class GeminiParser extends ChatParser {
|
|
8
18
|
name = "Gemini";
|
|
9
19
|
|
|
@@ -84,7 +94,23 @@ export class GeminiParser extends ChatParser {
|
|
|
84
94
|
|
|
85
95
|
const mode = options.parserMode || "auto";
|
|
86
96
|
|
|
87
|
-
//
|
|
97
|
+
// 1. Prefer DOM extraction first when on a live page with conversation containers
|
|
98
|
+
if (typeof document !== "undefined" && document.querySelector) {
|
|
99
|
+
const hasDomMessages = document.querySelector(
|
|
100
|
+
".conversation-container, user-query, model-response, deep-research-immersive-panel",
|
|
101
|
+
);
|
|
102
|
+
if (hasDomMessages && mode !== "api") {
|
|
103
|
+
const domResult = this.parseFromDom(currentUrl, options);
|
|
104
|
+
if (domResult && domResult.messages && domResult.messages.length > 0) {
|
|
105
|
+
console.log(
|
|
106
|
+
`[Gemini Parser] Successfully parsed ${domResult.messages.length} messages from DOM`,
|
|
107
|
+
);
|
|
108
|
+
return domResult;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Attempt API / RPC extraction if DOM parsing didn't find messages or mode is API
|
|
88
114
|
if (mode !== "dom" && typeof fetch === "function") {
|
|
89
115
|
try {
|
|
90
116
|
const convoId = this.getConversationId(currentUrl);
|
|
@@ -104,7 +130,10 @@ export class GeminiParser extends ChatParser {
|
|
|
104
130
|
if (
|
|
105
131
|
apiResult &&
|
|
106
132
|
apiResult.messages &&
|
|
107
|
-
apiResult.messages.length > 0
|
|
133
|
+
apiResult.messages.length > 0 &&
|
|
134
|
+
apiResult.messages.some((m) =>
|
|
135
|
+
isValidMessageText(m.content, convoId),
|
|
136
|
+
)
|
|
108
137
|
) {
|
|
109
138
|
console.log(
|
|
110
139
|
`[Gemini Parser] Successfully parsed ${apiResult.messages.length} messages via API`,
|
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);
|