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 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
- // Attempt API / RPC extraction first when in auto mode and in real browser
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decant-core",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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);