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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decant-core",
3
- "version": "1.0.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": "Rat-S",
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 '../lib/turndown.js';
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: '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
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('lineBreak', {
42
- filter: ['br'],
43
- replacement: () => ' \n',
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('qwenCodeBlock', {
47
+ turndownService.addRule("qwenCodeBlock", {
48
48
  filter: function (node) {
49
- return node.nodeName === 'PRE' && node.classList.contains('qwen-markdown-code');
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('.qwen-markdown-code-header div');
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('.view-line');
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('span');
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('\n');
75
- if (!code.trim()) return '';
76
+ const code = codeLines.join("\n");
77
+ if (!code.trim()) return "";
76
78
 
77
- return '\n```' + language + '\n' + code + '\n```\n';
79
+ return "\n```" + language + "\n" + code + "\n```\n";
78
80
  },
79
81
  });
80
82
 
81
83
  // Add table conversion rule
82
- turndownService.addRule('tables', {
83
- filter: 'table',
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('tr'));
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('lineBreak', {
101
- filter: ['br'],
102
- replacement: () => '<br>',
102
+ cellTurndown.addRule("lineBreak", {
103
+ filter: ["br"],
104
+ replacement: () => "<br>",
103
105
  });
104
106
 
105
- let markdown = '\n';
107
+ let markdown = "\n";
106
108
  rows.forEach((row, rowIndex) => {
107
- const cells = Array.from(row.querySelectorAll('th, td'));
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, '<br>');
116
+ return cellMarkdown.trim().replace(/\n/g, "<br>");
115
117
  });
116
118
 
117
119
  // Add row
118
- markdown += '| ' + cellContents.join(' | ') + ' |\n';
120
+ markdown += "| " + cellContents.join(" | ") + " |\n";
119
121
 
120
122
  // Add separator after header row
121
123
  if (rowIndex === 0) {
122
- markdown += '| ' + cells.map(() => '---').join(' | ') + ' |\n';
124
+ markdown += "| " + cells.map(() => "---").join(" | ") + " |\n";
123
125
  }
124
126
  });
125
127
 
126
- return markdown + '\n';
128
+ return markdown + "\n";
127
129
  },
128
130
  });
129
131
 
130
132
  // Convert the HTML
131
133
  let html;
132
- if (typeof htmlContent === 'string') {
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 === 'function')
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('.katex-display').forEach((el) => {
147
- const annotation = el.querySelector('annotation[encoding="application/x-tex"]');
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(`\n\n$$${latex}$$\n\n`);
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(`\n\n$$${el.textContent.trim()}$$\n\n`);
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('.katex').forEach((el) => {
167
+ clone.querySelectorAll(".katex").forEach((el) => {
160
168
  if (!el.parentNode) return;
161
- const annotation = el.querySelector('annotation[encoding="application/x-tex"]');
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(`$${el.textContent.trim()}$`);
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('[data-math]').forEach((el) => {
185
+ clone.querySelectorAll("[data-math]").forEach((el) => {
174
186
  if (!el.parentNode) return;
175
- const latex = el.getAttribute('data-math');
176
- const isBlock = el.classList.contains('math-block') || el.tagName === 'DIV';
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('[data-xpm-latex]').forEach((el) => {
184
- const copyRoot = el.closest('[data-xpm-copy-root]');
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('.cPGBZb') || copyRoot;
199
+ const container = el.closest(".cPGBZb") || copyRoot;
187
200
  if (!container.parentNode) return;
188
201
 
189
- const latex = el.getAttribute('data-xpm-latex');
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.replace(container.textContent, '').trim();
196
- isBlock = parentText === '';
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('pre').forEach((pre) => {
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('pre');
210
- const codeElement = pre.querySelector('code');
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('svg');
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('br').forEach((br) => {
239
+ codeElement.querySelectorAll("br").forEach((br) => {
225
240
  if (br.parentNode) {
226
- const newline = clone.ownerDocument.createTextNode('\n');
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('pre');
235
- const newCode = clone.ownerDocument.createElement('code');
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('code');
264
+ const code = pre.querySelector("code");
250
265
  const hasLanguage =
251
266
  code &&
252
267
  Array.from(code.classList).some(
253
- (cls) => cls.startsWith('language-') && cls !== 'language-',
268
+ (cls) => cls.startsWith("language-") && cls !== "language-",
254
269
  );
255
270
  if (!hasLanguage) {
256
- const container = pre.closest('.pHpOfb') || pre.parentElement?.parentElement;
271
+ const container =
272
+ pre.closest(".pHpOfb") || pre.parentElement?.parentElement;
257
273
  if (container) {
258
274
  const langEl =
259
- container.querySelector('.vVRw1d') || container.firstElementChild?.firstElementChild;
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('code') || pre;
272
- codeEl.querySelectorAll('br').forEach((br) => {
288
+ const codeEl = pre.querySelector("code") || pre;
289
+ codeEl.querySelectorAll("br").forEach((br) => {
273
290
  if (br.parentNode) {
274
- const newline = clone.ownerDocument.createTextNode('\n');
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('*').forEach((el) => {
283
- if (el.textContent.trim() === 'Use code with caution.') {
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('button').forEach((button) => {
290
- const img = button.querySelector('img');
306
+ clone.querySelectorAll("button").forEach((button) => {
307
+ const img = button.querySelector("img");
291
308
  if (!img) return;
292
309
 
293
- let caption = 'Image';
294
- const ariaLabel = button.getAttribute('aria-label') || '';
295
- if (ariaLabel.toLowerCase().includes('open image details for')) {
296
- caption = ariaLabel.replace(/^Open image details for\s*/i, '').trim();
297
- } else if (img.getAttribute('alt') && !img.getAttribute('alt').startsWith('http')) {
298
- caption = img.getAttribute('alt').trim();
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('alt') || '';
302
- const src = img.getAttribute('src') || '';
303
- const imageUrl = alt.startsWith('http://') || alt.startsWith('https://') ? alt : src;
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('img');
307
- newImg.setAttribute('src', imageUrl);
308
- newImg.setAttribute('alt', caption);
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
- 'button:not(.qwen-markdown-code *)',
317
- '.copy-button',
337
+ "button:not(.qwen-markdown-code *)",
338
+ ".copy-button",
318
339
  '[role="button"]:not(.qwen-markdown-code *)',
319
- '.sr-only',
320
- 'svg:not(.qwen-markdown-code *)',
321
- '.icon:not(.qwen-markdown-code *)',
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
- 'div.HvurC',
326
- '.DBd2Wb',
327
- '.oLpkLe',
328
- '.OUQe0e',
329
- '.UrecDd',
346
+ "div.HvurC",
347
+ ".DBd2Wb",
348
+ ".oLpkLe",
349
+ ".OUQe0e",
350
+ ".UrecDd",
330
351
  '[aria-label="Share public link"]',
331
- 'aside.L9AUvd',
332
- 'aside.UL0w9b',
333
- 'div.qacuz',
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('Invalid input to convertToMarkdown:', htmlContent);
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('Error converting HTML to markdown:', 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, 'text/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, '\n\n')
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
  }