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