decant-core 1.0.0 → 1.0.1
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/README.md +20 -20
- package/ai/base.js +4 -4
- package/ai/chatgpt.js +356 -216
- package/ai/chatgpt_helper.js +46 -28
- package/ai/chatgpt_scroll_collector.js +36 -33
- package/ai/chub.js +90 -0
- package/ai/claude.js +119 -92
- package/ai/claude_react_reader.js +8 -6
- package/ai/copilot.js +158 -124
- package/ai/deepseek.js +36 -27
- package/ai/gemini.js +384 -230
- package/ai/gemini_cloud_assist.js +31 -22
- package/ai/google_ai_studio.js +45 -27
- package/ai/google_search_ai.js +32 -23
- package/ai/index.js +25 -19
- package/ai/joyland.js +85 -0
- package/ai/lumo.js +39 -28
- package/ai/meta.js +30 -24
- package/ai/mistral.js +21 -16
- package/ai/notebooklm.js +50 -34
- package/ai/perplexity.js +22 -19
- package/ai/qwen.js +29 -25
- package/ai/z_ai.js +34 -28
- package/detection/detect-platform.js +27 -19
- package/detection/domains.js +32 -24
- package/lib/turndown.js +352 -183
- package/package.json +13 -4
- package/utils/html-to-markdown.js +130 -109
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decant-core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Shared AI chat platform parsers and detection for Covai browser extensions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ai/index.js",
|
|
@@ -41,16 +41,25 @@
|
|
|
41
41
|
"copilot",
|
|
42
42
|
"browser-extension"
|
|
43
43
|
],
|
|
44
|
-
"author": "
|
|
44
|
+
"author": "deadrat-in",
|
|
45
45
|
"license": "AGPL-3.0-only",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
46
49
|
"dependencies": {
|
|
47
50
|
"turndown": "^7.2.4"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
53
|
"@eslint/js": "^10.0.1",
|
|
54
|
+
"@semantic-release/changelog": "^7.0.0",
|
|
55
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
56
|
+
"@semantic-release/git": "^11.0.1",
|
|
57
|
+
"@semantic-release/github": "^12.0.9",
|
|
58
|
+
"@semantic-release/npm": "^13.1.5",
|
|
59
|
+
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
51
60
|
"eslint": "^10.8.0",
|
|
52
61
|
"globals": "^17.9.0",
|
|
53
|
-
"prettier": "^3.9.6"
|
|
62
|
+
"prettier": "^3.9.6",
|
|
63
|
+
"semantic-release": "^25.0.9"
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
|
-
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @param {Object} options - Optional configuration for the conversion
|
|
13
13
|
* @returns {string} Markdown formatted text
|
|
14
14
|
*/
|
|
15
|
-
import TurndownService from
|
|
15
|
+
import TurndownService from "../lib/turndown.js";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Convert an HTML element to markdown
|
|
@@ -23,177 +23,192 @@ import TurndownService from '../lib/turndown.js';
|
|
|
23
23
|
export function convertToMarkdown(htmlContent, options = {}) {
|
|
24
24
|
// Configure Turndown service
|
|
25
25
|
const turndownService = new TurndownService({
|
|
26
|
-
headingStyle:
|
|
27
|
-
hr:
|
|
28
|
-
bulletListMarker:
|
|
29
|
-
codeBlockStyle:
|
|
30
|
-
fence:
|
|
31
|
-
emDelimiter:
|
|
32
|
-
strongDelimiter:
|
|
33
|
-
linkStyle:
|
|
34
|
-
linkReferenceStyle:
|
|
26
|
+
headingStyle: "atx", // Use # for headings (not underline style)
|
|
27
|
+
hr: "* * *", // Horizontal rules
|
|
28
|
+
bulletListMarker: "*", // Use * for bullet lists
|
|
29
|
+
codeBlockStyle: "fenced", // Use ``` for code blocks
|
|
30
|
+
fence: "```", // Code block fence characters
|
|
31
|
+
emDelimiter: "*", // Use * for emphasis
|
|
32
|
+
strongDelimiter: "**", // Use ** for strong
|
|
33
|
+
linkStyle: "inlined", // [text](url) format
|
|
34
|
+
linkReferenceStyle: "full", // Full reference style for links
|
|
35
35
|
...options,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
// Add custom rules for better conversion
|
|
39
39
|
|
|
40
40
|
// Preserve line breaks
|
|
41
|
-
turndownService.addRule(
|
|
42
|
-
filter: [
|
|
43
|
-
replacement: () =>
|
|
41
|
+
turndownService.addRule("lineBreak", {
|
|
42
|
+
filter: ["br"],
|
|
43
|
+
replacement: () => " \n",
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
// Handle Qwen's Monaco editor code blocks
|
|
47
|
-
turndownService.addRule(
|
|
47
|
+
turndownService.addRule("qwenCodeBlock", {
|
|
48
48
|
filter: function (node) {
|
|
49
|
-
return
|
|
49
|
+
return (
|
|
50
|
+
node.nodeName === "PRE" && node.classList.contains("qwen-markdown-code")
|
|
51
|
+
);
|
|
50
52
|
},
|
|
51
53
|
replacement: function (content, node) {
|
|
52
54
|
// Extract language from header
|
|
53
|
-
let language =
|
|
54
|
-
const headerDiv = node.querySelector(
|
|
55
|
+
let language = "";
|
|
56
|
+
const headerDiv = node.querySelector(".qwen-markdown-code-header div");
|
|
55
57
|
if (headerDiv) {
|
|
56
58
|
language = headerDiv.textContent.trim();
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// Extract code from Monaco editor view-lines
|
|
60
|
-
const viewLines = node.querySelectorAll(
|
|
62
|
+
const viewLines = node.querySelectorAll(".view-line");
|
|
61
63
|
const codeLines = [];
|
|
62
64
|
viewLines.forEach((line) => {
|
|
63
65
|
// Get text content, preserving structure
|
|
64
|
-
let lineText =
|
|
65
|
-
const spans = line.querySelectorAll(
|
|
66
|
+
let lineText = "";
|
|
67
|
+
const spans = line.querySelectorAll("span");
|
|
66
68
|
spans.forEach((span) => {
|
|
67
69
|
// Replace with spaces and get text
|
|
68
|
-
const text = span.textContent.replace(/\u00A0/g,
|
|
70
|
+
const text = span.textContent.replace(/\u00A0/g, " ");
|
|
69
71
|
lineText += text;
|
|
70
72
|
});
|
|
71
73
|
codeLines.push(lineText);
|
|
72
74
|
});
|
|
73
75
|
|
|
74
|
-
const code = codeLines.join(
|
|
75
|
-
if (!code.trim()) return
|
|
76
|
+
const code = codeLines.join("\n");
|
|
77
|
+
if (!code.trim()) return "";
|
|
76
78
|
|
|
77
|
-
return
|
|
79
|
+
return "\n```" + language + "\n" + code + "\n```\n";
|
|
78
80
|
},
|
|
79
81
|
});
|
|
80
82
|
|
|
81
83
|
// Add table conversion rule
|
|
82
|
-
turndownService.addRule(
|
|
83
|
-
filter:
|
|
84
|
+
turndownService.addRule("tables", {
|
|
85
|
+
filter: "table",
|
|
84
86
|
replacement: function (content, node) {
|
|
85
87
|
// Convert HTML table to markdown table
|
|
86
|
-
const rows = Array.from(node.querySelectorAll(
|
|
88
|
+
const rows = Array.from(node.querySelectorAll("tr"));
|
|
87
89
|
if (rows.length === 0) return content;
|
|
88
90
|
|
|
89
91
|
// Create a separate Turndown service for cell content to avoid recursion/state issues
|
|
90
92
|
// and incorrectly stripping or double-processing content
|
|
91
93
|
const cellTurndown = new TurndownService({
|
|
92
|
-
emDelimiter:
|
|
93
|
-
strongDelimiter:
|
|
94
|
+
emDelimiter: "*",
|
|
95
|
+
strongDelimiter: "**",
|
|
94
96
|
});
|
|
95
97
|
|
|
96
98
|
// Handle breaks in cells
|
|
97
99
|
// Convert BR to newlines first so Turndown sees them as breaks (or handle directly)
|
|
98
100
|
// Actually Turndown by default drops BRs or converts to newline.
|
|
99
101
|
// We want to force them to <br> string for table cells.
|
|
100
|
-
cellTurndown.addRule(
|
|
101
|
-
filter: [
|
|
102
|
-
replacement: () =>
|
|
102
|
+
cellTurndown.addRule("lineBreak", {
|
|
103
|
+
filter: ["br"],
|
|
104
|
+
replacement: () => "<br>",
|
|
103
105
|
});
|
|
104
106
|
|
|
105
|
-
let markdown =
|
|
107
|
+
let markdown = "\n";
|
|
106
108
|
rows.forEach((row, rowIndex) => {
|
|
107
|
-
const cells = Array.from(row.querySelectorAll(
|
|
109
|
+
const cells = Array.from(row.querySelectorAll("th, td"));
|
|
108
110
|
|
|
109
111
|
// Convert each cell's HTML to markdown using the isolated service
|
|
110
112
|
const cellContents = cells.map((cell) => {
|
|
111
113
|
let cellMarkdown = cellTurndown.turndown(cell.innerHTML);
|
|
112
114
|
// Replace any actual newlines that Turndown generated (e.g. from P tags) with <br>
|
|
113
115
|
// as tables cannot have literal newlines in GFM.
|
|
114
|
-
return cellMarkdown.trim().replace(/\n/g,
|
|
116
|
+
return cellMarkdown.trim().replace(/\n/g, "<br>");
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
// Add row
|
|
118
|
-
markdown +=
|
|
120
|
+
markdown += "| " + cellContents.join(" | ") + " |\n";
|
|
119
121
|
|
|
120
122
|
// Add separator after header row
|
|
121
123
|
if (rowIndex === 0) {
|
|
122
|
-
markdown +=
|
|
124
|
+
markdown += "| " + cells.map(() => "---").join(" | ") + " |\n";
|
|
123
125
|
}
|
|
124
126
|
});
|
|
125
127
|
|
|
126
|
-
return markdown +
|
|
128
|
+
return markdown + "\n";
|
|
127
129
|
},
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
// Convert the HTML
|
|
131
133
|
let html;
|
|
132
|
-
if (typeof htmlContent ===
|
|
134
|
+
if (typeof htmlContent === "string") {
|
|
133
135
|
html = htmlContent;
|
|
134
136
|
} else if (
|
|
135
137
|
htmlContent &&
|
|
136
138
|
(htmlContent instanceof HTMLElement ||
|
|
137
139
|
htmlContent.nodeType === 1 ||
|
|
138
140
|
htmlContent.nodeType === 9 ||
|
|
139
|
-
typeof htmlContent.querySelectorAll ===
|
|
141
|
+
typeof htmlContent.querySelectorAll === "function")
|
|
140
142
|
) {
|
|
141
143
|
// Clone the element to avoid modifying the original
|
|
142
144
|
const clone = htmlContent.cloneNode(true);
|
|
143
145
|
|
|
144
146
|
// Convert math equations (KaTeX and data-math) to standard markdown math blocks
|
|
145
147
|
// 1. Process block display KaTeX
|
|
146
|
-
clone.querySelectorAll(
|
|
147
|
-
const annotation = el.querySelector(
|
|
148
|
+
clone.querySelectorAll(".katex-display").forEach((el) => {
|
|
149
|
+
const annotation = el.querySelector(
|
|
150
|
+
'annotation[encoding="application/x-tex"]',
|
|
151
|
+
);
|
|
148
152
|
if (annotation) {
|
|
149
153
|
const latex = annotation.textContent.trim();
|
|
150
|
-
const textNode = clone.ownerDocument.createTextNode(
|
|
154
|
+
const textNode = clone.ownerDocument.createTextNode(
|
|
155
|
+
`\n\n$$${latex}$$\n\n`,
|
|
156
|
+
);
|
|
151
157
|
el.parentNode.replaceChild(textNode, el);
|
|
152
158
|
} else {
|
|
153
|
-
const textNode = clone.ownerDocument.createTextNode(
|
|
159
|
+
const textNode = clone.ownerDocument.createTextNode(
|
|
160
|
+
`\n\n$$${el.textContent.trim()}$$\n\n`,
|
|
161
|
+
);
|
|
154
162
|
el.parentNode.replaceChild(textNode, el);
|
|
155
163
|
}
|
|
156
164
|
});
|
|
157
165
|
|
|
158
166
|
// 2. Process inline KaTeX
|
|
159
|
-
clone.querySelectorAll(
|
|
167
|
+
clone.querySelectorAll(".katex").forEach((el) => {
|
|
160
168
|
if (!el.parentNode) return;
|
|
161
|
-
const annotation = el.querySelector(
|
|
169
|
+
const annotation = el.querySelector(
|
|
170
|
+
'annotation[encoding="application/x-tex"]',
|
|
171
|
+
);
|
|
162
172
|
if (annotation) {
|
|
163
173
|
const latex = annotation.textContent.trim();
|
|
164
174
|
const textNode = clone.ownerDocument.createTextNode(`$${latex}$`);
|
|
165
175
|
el.parentNode.replaceChild(textNode, el);
|
|
166
176
|
} else {
|
|
167
|
-
const textNode = clone.ownerDocument.createTextNode(
|
|
177
|
+
const textNode = clone.ownerDocument.createTextNode(
|
|
178
|
+
`$${el.textContent.trim()}$`,
|
|
179
|
+
);
|
|
168
180
|
el.parentNode.replaceChild(textNode, el);
|
|
169
181
|
}
|
|
170
182
|
});
|
|
171
183
|
|
|
172
184
|
// 3. Process Gemini-style data-math attributes
|
|
173
|
-
clone.querySelectorAll(
|
|
185
|
+
clone.querySelectorAll("[data-math]").forEach((el) => {
|
|
174
186
|
if (!el.parentNode) return;
|
|
175
|
-
const latex = el.getAttribute(
|
|
176
|
-
const isBlock =
|
|
187
|
+
const latex = el.getAttribute("data-math");
|
|
188
|
+
const isBlock =
|
|
189
|
+
el.classList.contains("math-block") || el.tagName === "DIV";
|
|
177
190
|
const replacementText = isBlock ? `\n\n$$${latex}$$\n\n` : `$${latex}$`;
|
|
178
191
|
const textNode = clone.ownerDocument.createTextNode(replacementText);
|
|
179
192
|
el.parentNode.replaceChild(textNode, el);
|
|
180
193
|
});
|
|
181
194
|
|
|
182
195
|
// 4. Process Google Search SGE LaTeX images with [data-xpm-latex]
|
|
183
|
-
clone.querySelectorAll(
|
|
184
|
-
const copyRoot = el.closest(
|
|
196
|
+
clone.querySelectorAll("[data-xpm-latex]").forEach((el) => {
|
|
197
|
+
const copyRoot = el.closest("[data-xpm-copy-root]");
|
|
185
198
|
if (!copyRoot) return;
|
|
186
|
-
const container = el.closest(
|
|
199
|
+
const container = el.closest(".cPGBZb") || copyRoot;
|
|
187
200
|
if (!container.parentNode) return;
|
|
188
201
|
|
|
189
|
-
const latex = el.getAttribute(
|
|
202
|
+
const latex = el.getAttribute("data-xpm-latex");
|
|
190
203
|
|
|
191
204
|
// Determine if it is block math
|
|
192
205
|
let isBlock = false;
|
|
193
206
|
const parent = container.parentNode;
|
|
194
207
|
if (parent) {
|
|
195
|
-
const parentText = parent.textContent
|
|
196
|
-
|
|
208
|
+
const parentText = parent.textContent
|
|
209
|
+
.replace(container.textContent, "")
|
|
210
|
+
.trim();
|
|
211
|
+
isBlock = parentText === "";
|
|
197
212
|
}
|
|
198
213
|
|
|
199
214
|
const replacementText = isBlock ? `\n\n$$${latex}$$\n\n` : `$${latex}$`;
|
|
@@ -202,18 +217,18 @@ export function convertToMarkdown(htmlContent, options = {}) {
|
|
|
202
217
|
});
|
|
203
218
|
|
|
204
219
|
// Preprocess code blocks (pre elements) to normalize formatting and language tags
|
|
205
|
-
clone.querySelectorAll(
|
|
220
|
+
clone.querySelectorAll("pre").forEach((pre) => {
|
|
206
221
|
// Guard against processing detached/already-replaced pre elements
|
|
207
222
|
if (!clone.contains(pre)) return;
|
|
208
223
|
|
|
209
|
-
const innerPre = pre.querySelector(
|
|
210
|
-
const codeElement = pre.querySelector(
|
|
224
|
+
const innerPre = pre.querySelector("pre");
|
|
225
|
+
const codeElement = pre.querySelector("code");
|
|
211
226
|
|
|
212
227
|
if (innerPre && codeElement) {
|
|
213
228
|
// --- ChatGPT-style wrapped code block ---
|
|
214
229
|
// Find the language label from the header (usually next to an SVG icon)
|
|
215
|
-
let language =
|
|
216
|
-
const svg = pre.querySelector(
|
|
230
|
+
let language = "";
|
|
231
|
+
const svg = pre.querySelector("svg");
|
|
217
232
|
if (svg && svg.parentElement) {
|
|
218
233
|
const parent = svg.parentElement;
|
|
219
234
|
svg.remove();
|
|
@@ -221,9 +236,9 @@ export function convertToMarkdown(htmlContent, options = {}) {
|
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
// Replace all <br> elements inside the code block with actual newlines
|
|
224
|
-
codeElement.querySelectorAll(
|
|
239
|
+
codeElement.querySelectorAll("br").forEach((br) => {
|
|
225
240
|
if (br.parentNode) {
|
|
226
|
-
const newline = clone.ownerDocument.createTextNode(
|
|
241
|
+
const newline = clone.ownerDocument.createTextNode("\n");
|
|
227
242
|
br.parentNode.replaceChild(newline, br);
|
|
228
243
|
}
|
|
229
244
|
});
|
|
@@ -231,8 +246,8 @@ export function convertToMarkdown(htmlContent, options = {}) {
|
|
|
231
246
|
const codeText = codeElement.textContent;
|
|
232
247
|
|
|
233
248
|
// Create a new clean pre and code element structure
|
|
234
|
-
const newPre = clone.ownerDocument.createElement(
|
|
235
|
-
const newCode = clone.ownerDocument.createElement(
|
|
249
|
+
const newPre = clone.ownerDocument.createElement("pre");
|
|
250
|
+
const newCode = clone.ownerDocument.createElement("code");
|
|
236
251
|
if (language) {
|
|
237
252
|
newCode.className = `language-${language}`;
|
|
238
253
|
}
|
|
@@ -246,17 +261,19 @@ export function convertToMarkdown(htmlContent, options = {}) {
|
|
|
246
261
|
} else {
|
|
247
262
|
// --- Standard/SGE-style code block ---
|
|
248
263
|
// Extract language label if available (e.g. from Google Search SGE containers)
|
|
249
|
-
const code = pre.querySelector(
|
|
264
|
+
const code = pre.querySelector("code");
|
|
250
265
|
const hasLanguage =
|
|
251
266
|
code &&
|
|
252
267
|
Array.from(code.classList).some(
|
|
253
|
-
(cls) => cls.startsWith(
|
|
268
|
+
(cls) => cls.startsWith("language-") && cls !== "language-",
|
|
254
269
|
);
|
|
255
270
|
if (!hasLanguage) {
|
|
256
|
-
const container =
|
|
271
|
+
const container =
|
|
272
|
+
pre.closest(".pHpOfb") || pre.parentElement?.parentElement;
|
|
257
273
|
if (container) {
|
|
258
274
|
const langEl =
|
|
259
|
-
container.querySelector(
|
|
275
|
+
container.querySelector(".vVRw1d") ||
|
|
276
|
+
container.firstElementChild?.firstElementChild;
|
|
260
277
|
if (langEl && langEl !== pre) {
|
|
261
278
|
const language = langEl.textContent.trim().toLowerCase();
|
|
262
279
|
if (code) {
|
|
@@ -268,10 +285,10 @@ export function convertToMarkdown(htmlContent, options = {}) {
|
|
|
268
285
|
}
|
|
269
286
|
|
|
270
287
|
// Replace all <br> elements inside the code block with actual newlines
|
|
271
|
-
const codeEl = pre.querySelector(
|
|
272
|
-
codeEl.querySelectorAll(
|
|
288
|
+
const codeEl = pre.querySelector("code") || pre;
|
|
289
|
+
codeEl.querySelectorAll("br").forEach((br) => {
|
|
273
290
|
if (br.parentNode) {
|
|
274
|
-
const newline = clone.ownerDocument.createTextNode(
|
|
291
|
+
const newline = clone.ownerDocument.createTextNode("\n");
|
|
275
292
|
br.parentNode.replaceChild(newline, br);
|
|
276
293
|
}
|
|
277
294
|
});
|
|
@@ -279,33 +296,37 @@ export function convertToMarkdown(htmlContent, options = {}) {
|
|
|
279
296
|
});
|
|
280
297
|
|
|
281
298
|
// Remove SGE "Use code with caution" text blocks
|
|
282
|
-
clone.querySelectorAll(
|
|
283
|
-
if (el.textContent.trim() ===
|
|
299
|
+
clone.querySelectorAll("*").forEach((el) => {
|
|
300
|
+
if (el.textContent.trim() === "Use code with caution.") {
|
|
284
301
|
el.remove();
|
|
285
302
|
}
|
|
286
303
|
});
|
|
287
304
|
|
|
288
305
|
// Pre-process button-wrapped images (e.g., ChatGPT image carousels) before noise button removal
|
|
289
|
-
clone.querySelectorAll(
|
|
290
|
-
const img = button.querySelector(
|
|
306
|
+
clone.querySelectorAll("button").forEach((button) => {
|
|
307
|
+
const img = button.querySelector("img");
|
|
291
308
|
if (!img) return;
|
|
292
309
|
|
|
293
|
-
let caption =
|
|
294
|
-
const ariaLabel = button.getAttribute(
|
|
295
|
-
if (ariaLabel.toLowerCase().includes(
|
|
296
|
-
caption = ariaLabel.replace(/^Open image details for\s*/i,
|
|
297
|
-
} else if (
|
|
298
|
-
|
|
310
|
+
let caption = "Image";
|
|
311
|
+
const ariaLabel = button.getAttribute("aria-label") || "";
|
|
312
|
+
if (ariaLabel.toLowerCase().includes("open image details for")) {
|
|
313
|
+
caption = ariaLabel.replace(/^Open image details for\s*/i, "").trim();
|
|
314
|
+
} else if (
|
|
315
|
+
img.getAttribute("alt") &&
|
|
316
|
+
!img.getAttribute("alt").startsWith("http")
|
|
317
|
+
) {
|
|
318
|
+
caption = img.getAttribute("alt").trim();
|
|
299
319
|
}
|
|
300
320
|
|
|
301
|
-
const alt = img.getAttribute(
|
|
302
|
-
const src = img.getAttribute(
|
|
303
|
-
const imageUrl =
|
|
321
|
+
const alt = img.getAttribute("alt") || "";
|
|
322
|
+
const src = img.getAttribute("src") || "";
|
|
323
|
+
const imageUrl =
|
|
324
|
+
alt.startsWith("http://") || alt.startsWith("https://") ? alt : src;
|
|
304
325
|
|
|
305
326
|
if (imageUrl) {
|
|
306
|
-
const newImg = clone.ownerDocument.createElement(
|
|
307
|
-
newImg.setAttribute(
|
|
308
|
-
newImg.setAttribute(
|
|
327
|
+
const newImg = clone.ownerDocument.createElement("img");
|
|
328
|
+
newImg.setAttribute("src", imageUrl);
|
|
329
|
+
newImg.setAttribute("alt", caption);
|
|
309
330
|
button.parentNode.replaceChild(newImg, button);
|
|
310
331
|
}
|
|
311
332
|
});
|
|
@@ -313,24 +334,24 @@ export function convertToMarkdown(htmlContent, options = {}) {
|
|
|
313
334
|
// Remove noise elements (buttons, icons, etc.)
|
|
314
335
|
// Note: Exclude elements inside qwen-markdown-code to preserve Monaco editor content
|
|
315
336
|
const noiseSelectors = [
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
"button:not(.qwen-markdown-code *)",
|
|
338
|
+
".copy-button",
|
|
318
339
|
'[role="button"]:not(.qwen-markdown-code *)',
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
340
|
+
".sr-only",
|
|
341
|
+
"svg:not(.qwen-markdown-code *)",
|
|
342
|
+
".icon:not(.qwen-markdown-code *)",
|
|
322
343
|
'[aria-hidden="true"]:not(.qwen-markdown-code *)',
|
|
323
344
|
'[id^="shrproxy"]',
|
|
324
345
|
'[id^="fbproxy"]',
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
346
|
+
"div.HvurC",
|
|
347
|
+
".DBd2Wb",
|
|
348
|
+
".oLpkLe",
|
|
349
|
+
".OUQe0e",
|
|
350
|
+
".UrecDd",
|
|
330
351
|
'[aria-label="Share public link"]',
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
352
|
+
"aside.L9AUvd",
|
|
353
|
+
"aside.UL0w9b",
|
|
354
|
+
"div.qacuz",
|
|
334
355
|
];
|
|
335
356
|
|
|
336
357
|
noiseSelectors.forEach((selector) => {
|
|
@@ -339,19 +360,19 @@ export function convertToMarkdown(htmlContent, options = {}) {
|
|
|
339
360
|
|
|
340
361
|
html = clone;
|
|
341
362
|
} else {
|
|
342
|
-
console.warn(
|
|
343
|
-
return
|
|
363
|
+
console.warn("Invalid input to convertToMarkdown:", htmlContent);
|
|
364
|
+
return "";
|
|
344
365
|
}
|
|
345
366
|
|
|
346
367
|
try {
|
|
347
368
|
const markdown = turndownService.turndown(html);
|
|
348
369
|
return markdown.trim();
|
|
349
370
|
} catch (error) {
|
|
350
|
-
console.error(
|
|
371
|
+
console.error("Error converting HTML to markdown:", error);
|
|
351
372
|
// Fallback to plain text
|
|
352
373
|
const parser = new DOMParser();
|
|
353
|
-
const doc = parser.parseFromString(html,
|
|
354
|
-
return doc.body.innerText || doc.body.textContent ||
|
|
374
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
375
|
+
return doc.body.innerText || doc.body.textContent || "";
|
|
355
376
|
}
|
|
356
377
|
}
|
|
357
378
|
|
|
@@ -364,11 +385,11 @@ export function cleanMarkdown(markdown) {
|
|
|
364
385
|
return (
|
|
365
386
|
markdown
|
|
366
387
|
// Remove excessive blank lines (max 2 consecutive)
|
|
367
|
-
.replace(/\n{3,}/g,
|
|
388
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
368
389
|
// Remove trailing whitespace from lines
|
|
369
|
-
.replace(/[ \t]+$/gm,
|
|
390
|
+
.replace(/[ \t]+$/gm, "")
|
|
370
391
|
// Normalize horizontal rules
|
|
371
|
-
.replace(/^(-{3,}|\*{3,}|_{3,})$/gm,
|
|
392
|
+
.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, "* * *")
|
|
372
393
|
.trim()
|
|
373
394
|
);
|
|
374
395
|
}
|