// ==UserScript== // @name Copy as Markdown for AI // @name:de Als Markdown für KI kopieren // @namespace https://github.com/marmoris-x/tampermonkey-scripts // @version 1.0.3 // @author marmoris-x // @description Select page elements with Ctrl/Cmd+Click and convert them to clean Markdown for AI prompts // @description:de Wähle Seitenelemente mit Strg/Cmd+Klick aus und konvertiere sie in sauberes Markdown für KI-Prompts // @license MIT // @icon https://media.licdn.com/dms/image/v2/D560BAQFyGRIuF2bqeQ/company-logo_200_200/B56ZfTe2EtHoAI-/0/1751599768258/crawl4ai_logo?e=2147483647&v=beta&t=skhRct8O4VaW42IwD7eC9Eqc9Pbavt7n6q7QgaJTQE8 // @supportURL https://github.com/marmoris-x/tampermonkey-scripts/issues // @downloadURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/Copy%20as%20Markdown%20for%20AI.user.js // @updateURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/Copy%20as%20Markdown%20for%20AI.user.js // @match *://*/* // @require https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js // @grant GM.download // @grant GM.setClipboard // @grant GM_addStyle // @grant GM_download // @grant GM_registerMenuCommand // @grant GM_setClipboard // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; class MarkdownConverter { constructor() { this.converters = { "H1": (el, ctx) => this.convertHeading(el, 1, ctx), "H2": (el, ctx) => this.convertHeading(el, 2, ctx), "H3": (el, ctx) => this.convertHeading(el, 3, ctx), "H4": (el, ctx) => this.convertHeading(el, 4, ctx), "H5": (el, ctx) => this.convertHeading(el, 5, ctx), "H6": (el, ctx) => this.convertHeading(el, 6, ctx), "P": (el, ctx) => this.convertParagraph(el, ctx), "A": (el, ctx) => this.convertLink(el, ctx), "IMG": (el, ctx) => this.convertImage(el, ctx), "UL": (el, ctx) => this.convertList(el, "ul", ctx), "OL": (el, ctx) => this.convertList(el, "ol", ctx), "LI": (el, ctx) => this.convertListItem(el, ctx), "TABLE": (el, ctx) => this.convertTable(el, ctx), "BLOCKQUOTE": (el, ctx) => this.convertBlockquote(el, ctx), "PRE": (el, ctx) => this.convertPreformatted(el, ctx), "CODE": (el, ctx) => this.convertCode(el, ctx), "HR": () => "\n---\n", "BR": () => " \n", "STRONG": async (el, ctx) => `**${await this.getTextContent(el, ctx)}**`, "B": async (el, ctx) => `**${await this.getTextContent(el, ctx)}**`, "EM": async (el, ctx) => `*${await this.getTextContent(el, ctx)}*`, "I": async (el, ctx) => `*${await this.getTextContent(el, ctx)}*`, "DEL": async (el, ctx) => `~~${await this.getTextContent(el, ctx)}~~`, "S": async (el, ctx) => `~~${await this.getTextContent(el, ctx)}~~`, "DIV": (el, ctx) => this.convertDiv(el, ctx), "SPAN": (el, ctx) => this.convertSpan(el, ctx), "ARTICLE": (el, ctx) => this.convertArticle(el, ctx), "SECTION": (el, ctx) => this.convertSection(el, ctx), "FIGURE": (el, ctx) => this.convertFigure(el, ctx), "FIGCAPTION": (el, ctx) => this.convertFigCaption(el, ctx), "VIDEO": (el, ctx) => this.convertVideo(el, ctx), "IFRAME": (el, ctx) => this.convertIframe(el, ctx), "DL": (el, ctx) => this.convertDefinitionList(el, ctx), "DT": (el, ctx) => this.convertDefinitionTerm(el, ctx), "DD": (el, ctx) => this.convertDefinitionDescription(el, ctx), "TR": (el, ctx) => this.convertTableRow(el, ctx) }; this.conversionContext = { listDepth: 0, inTable: false, inCode: false, preserveWhitespace: false, references: [], imageCount: 0, linkCount: 0 }; } async convert(elements, options = {}) { this.resetContext(); this.options = { includeImages: true, preserveTables: true, keepCodeFormatting: true, simplifyLayout: false, preserveLinks: true, ...options }; const parts = []; for (const element of elements) { const md = await this.convertElement(element, this.conversionContext); if (md.trim()) parts.push(md); } let result = parts.join("\n\n"); if (this.conversionContext.references.length > 0) { result += "\n\n" + this.generateReferences(); } return this.postProcess(result); } resetContext() { this.conversionContext = { listDepth: 0, inTable: false, inCode: false, preserveWhitespace: false, references: [], imageCount: 0, linkCount: 0 }; } async convertElement(element, context) { if (this.isHidden(element)) return ""; if (["SCRIPT", "STYLE", "NOSCRIPT"].includes(element.tagName)) return ""; const converter = this.converters[element.tagName]; if (converter) return await converter(element, context); return await this.processChildren(element, context); } async processChildren(element, context) { const parts = []; for (const child of element.childNodes) { if (child.nodeType === Node.TEXT_NODE) { const text = this.processTextNode(child, context); if (text) parts.push(text); } else if (child.nodeType === Node.ELEMENT_NODE) { const md = await this.convertElement(child, context); if (md) parts.push(md); } } return parts.join(""); } processTextNode(node, context) { let text = node.textContent; if (!context.preserveWhitespace && !context.inCode) { text = text.replace(/\s+/g, " "); if (this.isBlockBoundary(node.previousSibling)) text = text.trimStart(); if (this.isBlockBoundary(node.nextSibling)) text = text.trimEnd(); } if (!context.inCode) text = this.escapeMarkdown(text); return text; } isBlockBoundary(node) { if (!node || node.nodeType !== Node.ELEMENT_NODE) return true; const blocks = [ "DIV", "P", "H1", "H2", "H3", "H4", "H5", "H6", "UL", "OL", "LI", "BLOCKQUOTE", "PRE", "TABLE", "HR", "ARTICLE", "SECTION", "HEADER", "FOOTER", "NAV", "ASIDE", "MAIN" ]; return blocks.includes(node.tagName); } escapeMarkdown(text) { if (this.options.textOnly) return text; return text.replace(/\\/g, "\\\\").replace(/\*/g, "\\*").replace(/_/g, "\\_").replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\(/g, "\\(").replace(/\)/g, "\\)").replace(/\#/g, "\\#").replace(/\+/g, "\\+").replace(/\-/g, "\\-").replace(/\./g, "\\.").replace(/\!/g, "\\!").replace(/\|/g, "\\|"); } async convertHeading(element, level, context) { const text = await this.getTextContent(element, context); return "#".repeat(level) + " " + text + "\n"; } async convertParagraph(element, context) { const content = await this.processChildren(element, context); return content.trim() ? content + "\n" : ""; } async convertLink(element, context) { if (!this.options.preserveLinks || this.options.textOnly) { return await this.getTextContent(element, context); } const text = await this.getTextContent(element, context); const href = element.getAttribute("href"); const title = element.getAttribute("title"); if (!href) return text; const absoluteUrl = this.makeAbsoluteUrl(href); if (text && absoluteUrl) { return title ? `[${text}](${absoluteUrl} "${title}")` : `[${text}](${absoluteUrl})`; } return text; } async convertImage(element, context) { if (!this.options.includeImages || this.options.textOnly) { if (this.options.textOnly) { const alt2 = element.getAttribute("alt"); return alt2 ? `[Image: ${alt2}]` : ""; } return ""; } const src = element.getAttribute("src"); const alt = element.getAttribute("alt") || ""; const title = element.getAttribute("title"); if (!src) return ""; const absoluteUrl = this.makeAbsoluteUrl(src); return title ? `` : ``; } async convertList(element, type, context) { const oldDepth = context.listDepth; context.listDepth++; const items = []; for (const child of element.children) { if (child.tagName === "LI") { const md = await this.convertListItem(child, { ...context, listType: type }); if (md) items.push(md); } } context.listDepth = oldDepth; return items.join("\n") + (context.listDepth === 0 ? "\n" : ""); } async convertListItem(element, context) { const indent = " ".repeat(Math.max(0, context.listDepth - 1)); const bullet = context.listType === "ol" ? "1." : "-"; const content = (await this.processChildren(element, context)).trim(); return `${indent}${bullet} ${content}`; } async convertTable(element, context) { if (!this.options.preserveTables || this.options.textOnly) { return await this.convertTableToText(element, context); } const rows = []; const headerRows = []; let maxCols = 0; for (const child of element.children) { if (child.tagName === "THEAD") { for (const row of child.children) { if (row.tagName === "TR") { const cells = await this.processTableRow(row, context); headerRows.push(cells); maxCols = Math.max(maxCols, cells.length); } } } else if (child.tagName === "TBODY") { for (const row of child.children) { if (row.tagName === "TR") { const cells = await this.processTableRow(row, context); rows.push(cells); maxCols = Math.max(maxCols, cells.length); } } } else if (child.tagName === "TR") { const cells = await this.processTableRow(child, context); rows.push(cells); maxCols = Math.max(maxCols, cells.length); } } const markdownRows = []; if (headerRows.length > 0) { for (const hr of headerRows) { markdownRows.push("| " + this.padTableRow(hr, maxCols).join(" | ") + " |"); } markdownRows.push("| " + Array(maxCols).fill("---").join(" | ") + " |"); } for (const row of rows) { markdownRows.push("| " + this.padTableRow(row, maxCols).join(" | ") + " |"); } return markdownRows.join("\n") + "\n"; } async processTableRow(row, context) { const cells = []; for (const cell of row.children) { if (cell.tagName === "TD" || cell.tagName === "TH") { cells.push((await this.getTextContent(cell, context)).trim()); } } return cells; } async convertTableRow(element, context) { if (this.options.textOnly) { const cells2 = await this.processTableRow(element, context); return cells2.join(" "); } const cells = await this.processTableRow(element, context); return "| " + cells.join(" | ") + " |"; } padTableRow(row, target) { const padded = [...row]; while (padded.length < target) padded.push(""); return padded; } async convertTableToText(element, context) { const lines = []; for (const row of element.querySelectorAll("tr")) { const cellTexts = []; for (const cell of row.querySelectorAll("td, th")) { const text = (await this.getTextContent(cell, context)).trim(); if (text) cellTexts.push(text); } if (cellTexts.length > 0) lines.push(cellTexts.join(" ")); } return lines.join("\n"); } async convertBlockquote(element, context) { const lines = (await this.processChildren(element, context)).trim().split("\n"); return lines.map((l) => "> " + l).join("\n") + "\n"; } async convertPreformatted(element, context) { const oldInCode = context.inCode; const oldWs = context.preserveWhitespace; context.inCode = true; context.preserveWhitespace = true; let content = ""; let language = ""; const codeEl = element.querySelector("code"); if (codeEl) { const langMatch = (codeEl.className || "").match(/language-(\w+)/); if (langMatch) language = langMatch[1]; content = codeEl.textContent; } else { content = element.textContent; } context.inCode = oldInCode; context.preserveWhitespace = oldWs; return "```" + language + "\n" + content + "\n```\n"; } async convertCode(element, context) { if (element.parentElement && element.parentElement.tagName === "PRE") { return element.textContent; } return "`" + element.textContent + "`"; } async convertDiv(element, context) { if ((element.className || "").includes("code-block") || (element.className || "").includes("highlight")) { return await this.convertPreformatted(element, context); } const content = await this.processChildren(element, context); return content.trim() ? content + "\n" : ""; } async convertSpan(element, context) { if ((element.className || "").includes("code") || (element.className || "").includes("inline-code")) { return this.convertCode(element, context); } return await this.processChildren(element, context); } async convertArticle(element, context) { const content = await this.processChildren(element, context); return content.trim() ? content + "\n" : ""; } async convertSection(element, context) { const content = await this.processChildren(element, context); return content.trim() ? content + "\n" : ""; } async convertFigure(element, context) { const content = await this.processChildren(element, context); return content.trim() ? content + "\n" : ""; } async convertFigCaption(element, context) { const caption = await this.getTextContent(element, context); return caption ? "\n*" + caption + "*\n" : ""; } async convertVideo(element, context) { const title = element.getAttribute("title") || "Video"; if (this.options.textOnly) return `[Video: ${title}]`; const src = element.getAttribute("src"); const poster = element.getAttribute("poster"); if (!src) return ""; if (poster) { return `[})](${this.makeAbsoluteUrl(src)})`; } return `[${title}](${this.makeAbsoluteUrl(src)})`; } async convertIframe(element, context) { const title = element.getAttribute("title") || "Embedded content"; if (this.options.textOnly) { const src2 = element.getAttribute("src") || ""; if (src2.includes("youtube.com") || src2.includes("youtu.be")) return `[Video: ${title}]`; if (src2.includes("vimeo.com")) return `[Video: ${title}]`; return `[Embedded: ${title}]`; } const src = element.getAttribute("src"); if (!src) return ""; if (src.includes("youtube.com") || src.includes("youtu.be")) return `[▶️ ${title}](${src})`; if (src.includes("vimeo.com")) return `[▶️ ${title}](${src})`; return `[${title}](${src})`; } async convertDefinitionList(element, context) { return await this.processChildren(element, context) + "\n"; } async convertDefinitionTerm(element, context) { return "**" + await this.getTextContent(element, context) + "**\n"; } async convertDefinitionDescription(element, context) { return ": " + await this.processChildren(element, context) + "\n"; } async getTextContent(element, context) { if (context.inCode) return element.textContent; return await this.processChildren(element, context); } makeAbsoluteUrl(url) { if (!url) return ""; try { if (url.startsWith("http://") || url.startsWith("https://")) return url; if (url.startsWith("//")) return window.location.protocol + url; const base = window.location.origin; if (url.startsWith("/")) return base + url; const path = window.location.pathname; const pathDir = path.substring(0, path.lastIndexOf("/") + 1); return base + pathDir + url; } catch (e) { return url; } } isHidden(element) { const style = window.getComputedStyle(element); return style.display === "none" || style.visibility === "hidden" || style.opacity === "0"; } generateReferences() { return this.conversionContext.references.map((ref, i) => `[${i + 1}]: ${ref.url}`).join("\n"); } postProcess(markdown) { if (this.options.textOnly) markdown = this.postProcessTextOnly(markdown); markdown = markdown.replace(/\n{3,}/g, "\n\n"); markdown = markdown.replace(/ +([.,;:!?])/g, "$1"); markdown = markdown.replace(/\n(#{1,6} )/g, "\n\n$1"); markdown = markdown.replace(/(#{1,6} .+)\n(?![\n#])/g, "$1\n\n"); markdown = markdown.replace(/\n\n(-|\d+\.) /g, "\n$1 "); return markdown.trim(); } postProcessTextOnly(markdown) { const lines = markdown.split("\n"); const processed = []; let inMetadata = false; let currentItem = null; for (const rawLine of lines) { const line = rawLine.trim(); if (!line) { processed.push(""); continue; } const numMatch = line.match(/^(\d+)\.\s*(.+)$/); if (numMatch) { inMetadata = false; currentItem = numMatch[1]; const content = numMatch[2]; const domMatch = content.match(/^(.+?)\s*\(([^)]+)\)\s*(.*)$/); if (domMatch) { const [, title, domain, rest] = domMatch; processed.push(`${currentItem}. **${title.trim()}** (${domain})`); if (rest.trim()) { processed.push(` ${rest.trim()}`); inMetadata = true; } } else { processed.push(`${currentItem}. **${content}**`); } } else if (line.match(/\b(points?|by|ago|hide|comments?)\b/i) && currentItem) { const cleaned = line.replace(/\s+/g, " ").replace(/\s*\|\s*/g, " | ").trim(); processed.push(` ${cleaned}`); inMetadata = true; } else if (inMetadata && line.length < 100) { processed.push(` ${line}`); } else { inMetadata = false; processed.push(line); } } let result = processed.join("\n"); result = result.replace(/\n{3,}/g, "\n\n"); result = result.replace(/^(\d+\..+)$\n^(?!\s)/gm, "$1\n\n"); return result; } } class ContentAnalyzer { constructor() { this.patterns = { article: ["article", "main", "content", "post", "entry"], navigation: ["nav", "menu", "navigation", "breadcrumb"], sidebar: ["sidebar", "aside", "widget"], header: ["header", "masthead", "banner"], footer: ["footer", "copyright", "contact"], list: ["list", "items", "results", "products", "cards"], table: ["table", "grid", "data"], media: ["gallery", "carousel", "slideshow", "video", "media"] }; } async analyze(elements) { return { structure: this.analyzeStructure(elements), contentType: this.identifyContentType(elements), hierarchy: this.buildHierarchy(elements), mediaAssets: this.collectMediaAssets(elements), textDensity: this.calculateTextDensity(elements), semanticRegions: this.identifySemanticRegions(elements), relationships: this.analyzeRelationships(elements), metadata: this.extractMetadata(elements) }; } analyzeStructure(elements) { const structure = { hasHeadings: false, hasLists: false, hasTables: false, hasMedia: false, hasCode: false, hasLinks: false, layout: "linear", depth: 0, elementTypes: new Map() }; for (const element of elements) { this.analyzeElementStructure(element, structure); } structure.layout = this.determineLayout(elements); structure.depth = this.calculateMaxDepth(elements); return structure; } analyzeElementStructure(element, structure, visited = new Set()) { if (visited.has(element)) return; visited.add(element); const tagName = element.tagName; structure.elementTypes.set(tagName, (structure.elementTypes.get(tagName) || 0) + 1); if (/^H[1-6]$/.test(tagName)) structure.hasHeadings = true; else if (["UL", "OL", "DL"].includes(tagName)) structure.hasLists = true; else if (tagName === "TABLE") structure.hasTables = true; else if (["IMG", "VIDEO", "IFRAME", "PICTURE"].includes(tagName)) structure.hasMedia = true; else if (["CODE", "PRE"].includes(tagName)) structure.hasCode = true; else if (tagName === "A") structure.hasLinks = true; for (const child of element.children) { this.analyzeElementStructure(child, structure, visited); } } identifyContentType(elements) { const scores = { article: 0, list: 0, table: 0, form: 0, media: 0, mixed: 0 }; for (const element of elements) { const tagName = element.tagName; const className = element.className && typeof element.className === "string" ? element.className.toLowerCase() : ""; const id = (element.id || "").toLowerCase(); if (tagName === "ARTICLE" || this.matchesPattern(className + " " + id, this.patterns.article)) { scores.article += 10; } if (["UL", "OL"].includes(tagName) || this.matchesPattern(className, this.patterns.list)) { scores.list += 5; } if (tagName === "TABLE") scores.table += 10; if (tagName === "FORM" || element.querySelector("input, select, textarea")) scores.form += 5; if (this.matchesPattern(className, this.patterns.media) || element.querySelectorAll("img, video").length > 3) { scores.media += 5; } } const maxScore = Math.max(...Object.values(scores)); if (maxScore === 0) return "unknown"; for (const [type, score] of Object.entries(scores)) { if (score === maxScore) return type; } return "mixed"; } buildHierarchy(elements) { const hierarchy = { root: null, levels: [], headingStructure: [] }; if (elements.length > 0) { hierarchy.root = this.findCommonAncestor(elements); } const headings = []; for (const element of elements) { const found = element.querySelectorAll("h1, h2, h3, h4, h5, h6"); headings.push(...Array.from(found)); } headings.sort((a, b) => { const pos = a.compareDocumentPosition(b); if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; }); const stack = []; for (const heading of headings) { const level = parseInt(heading.tagName.substring(1)); const item = { level, text: heading.textContent.trim(), element: heading, children: [] }; while (stack.length > 0 && stack[stack.length - 1].level >= level) stack.pop(); if (stack.length > 0) { stack[stack.length - 1].children.push(item); } else { hierarchy.headingStructure.push(item); } stack.push(item); } return hierarchy; } collectMediaAssets(elements) { const media = { images: [], videos: [], iframes: [], audio: [] }; for (const element of elements) { element.querySelectorAll("img").forEach((img) => { media.images.push({ src: img.src, alt: img.alt, title: img.title, width: img.width, height: img.height, element: img }); }); element.querySelectorAll("video").forEach((video) => { media.videos.push({ src: video.src, poster: video.poster, width: video.width, height: video.height, element: video }); }); element.querySelectorAll("iframe").forEach((iframe) => { media.iframes.push({ src: iframe.src, width: iframe.width, height: iframe.height, title: iframe.title, element: iframe }); }); element.querySelectorAll("audio").forEach((audio) => { media.audio.push({ src: audio.src, element: audio }); }); } return media; } calculateTextDensity(elements) { let totalText = 0, totalElements = 0, linkText = 0, codeText = 0; for (const element of elements) { const stats = this.getTextStats(element); totalText += stats.textLength; totalElements += stats.elementCount; linkText += stats.linkTextLength; codeText += stats.codeTextLength; } return { textLength: totalText, elementCount: totalElements, averageTextPerElement: totalElements > 0 ? totalText / totalElements : 0, linkDensity: totalText > 0 ? linkText / totalText : 0, codeDensity: totalText > 0 ? codeText / totalText : 0 }; } getTextStats(element, visited = new Set()) { if (visited.has(element)) return { textLength: 0, elementCount: 0, linkTextLength: 0, codeTextLength: 0 }; visited.add(element); let stats = { textLength: 0, elementCount: 1, linkTextLength: 0, codeTextLength: 0 }; for (const node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent.trim(); stats.textLength += text.length; if (element.tagName === "A") stats.linkTextLength += text.length; if (["CODE", "PRE"].includes(element.tagName)) stats.codeTextLength += text.length; } } for (const child of element.children) { const cs = this.getTextStats(child, visited); stats.textLength += cs.textLength; stats.elementCount += cs.elementCount; stats.linkTextLength += cs.linkTextLength; stats.codeTextLength += cs.codeTextLength; } return stats; } identifySemanticRegions(elements) { const regions = { headers: [], navigation: [], main: [], sidebars: [], footers: [], articles: [] }; for (const element of elements) { let current = element; while (current) { const tagName = current.tagName; const className = current.className && typeof current.className === "string" ? current.className.toLowerCase() : ""; const role = current.getAttribute("role"); if (tagName === "HEADER" || role === "banner") regions.headers.push(current); else if (tagName === "NAV" || role === "navigation") regions.navigation.push(current); else if (tagName === "MAIN" || role === "main") regions.main.push(current); else if (tagName === "ASIDE" || role === "complementary") regions.sidebars.push(current); else if (tagName === "FOOTER" || role === "contentinfo") regions.footers.push(current); else if (tagName === "ARTICLE" || role === "article") regions.articles.push(current); if (this.matchesPattern(className, this.patterns.header)) regions.headers.push(current); else if (this.matchesPattern(className, this.patterns.navigation)) regions.navigation.push(current); else if (this.matchesPattern(className, this.patterns.sidebar)) regions.sidebars.push(current); else if (this.matchesPattern(className, this.patterns.footer)) regions.footers.push(current); current = current.parentElement; } } for (const key of Object.keys(regions)) { regions[key] = Array.from(new Set(regions[key])); } return regions; } analyzeRelationships(elements) { const relationships = { siblings: [], parents: [], children: [], relatedByClass: new Map(), relatedByStructure: [] }; for (let i = 0; i < elements.length; i++) { for (let j = i + 1; j < elements.length; j++) { if (elements[i].parentElement === elements[j].parentElement) { relationships.siblings.push([elements[i], elements[j]]); } } } for (const element of elements) { for (const other of elements) { if (element !== other) { if (element.contains(other)) relationships.parents.push({ parent: element, child: other }); else if (other.contains(element)) relationships.children.push({ parent: other, child: element }); } } } for (const element of elements) { const classes = Array.from(element.classList); for (const cls of classes) { if (!relationships.relatedByClass.has(cls)) relationships.relatedByClass.set(cls, []); relationships.relatedByClass.get(cls).push(element); } } for (let i = 0; i < elements.length; i++) { for (let j = i + 1; j < elements.length; j++) { if (this.areStructurallySimilar(elements[i], elements[j])) { relationships.relatedByStructure.push([elements[i], elements[j]]); } } } return relationships; } areStructurallySimilar(el1, el2) { if (el1.tagName !== el2.tagName) return false; const c1 = Array.from(el1.classList).sort(); const c2 = Array.from(el2.classList).sort(); const intersection = c1.filter((c) => c2.includes(c)); const union = Array.from( new Set([...c1, ...c2])); if (union.length > 0 && intersection.length / union.length >= 0.5) return true; if (el1.children.length === el2.children.length) { const t1 = Array.from(el1.children).map((c) => c.tagName).sort(); const t2 = Array.from(el2.children).map((c) => c.tagName).sort(); if (JSON.stringify(t1) === JSON.stringify(t2)) return true; } return false; } extractMetadata(elements) { const metadata = { title: null, description: null, author: null, date: null, tags: [], microdata: [] }; for (const element of elements) { const h1 = element.querySelector("h1"); if (h1 && !metadata.title) metadata.title = h1.textContent.trim(); const metas = element.querySelectorAll("[itemprop], [property], [name]"); for (const meta of metas) { const prop = meta.getAttribute("itemprop") || meta.getAttribute("property") || meta.getAttribute("name"); const content = meta.getAttribute("content") || meta.textContent.trim(); if (prop && content) { if (prop.includes("author")) metadata.author = content; else if (prop.includes("date") || prop.includes("time")) metadata.date = content; else if (prop.includes("description")) metadata.description = content; else if (prop.includes("tag") || prop.includes("keyword")) metadata.tags.push(content); metadata.microdata.push({ property: prop, value: content }); } } const times = element.querySelectorAll("time"); for (const time of times) { if (!metadata.date && time.dateTime) metadata.date = time.dateTime; } } return metadata; } determineLayout(elements) { const positions = elements.map((el) => { const r = el.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }); const rows = new Map(); for (const pos of positions) { const row = Math.round(pos.y / 10) * 10; if (!rows.has(row)) rows.set(row, []); rows.get(row).push(pos); } if (Array.from(rows.values()).some((r) => r.length > 1)) return "grid"; const widths = positions.map((p) => p.width); const avg = widths.reduce((a, b) => a + b, 0) / widths.length; const variance = widths.reduce((s, w) => s + Math.pow(w - avg, 2), 0) / widths.length; if (Math.sqrt(variance) / avg > 0.3) return "mixed"; return "linear"; } calculateMaxDepth(elements) { let max = 0; for (const el of elements) max = Math.max(max, this.getElementDepth(el)); return max; } getElementDepth(element, depth = 0) { if (element.children.length === 0) return depth; let maxChild = depth; for (const child of element.children) { maxChild = Math.max(maxChild, this.getElementDepth(child, depth + 1)); } return maxChild; } findCommonAncestor(elements) { if (elements.length === 0) return null; if (elements.length === 1) return elements[0].parentElement; let ancestor = elements[0]; const ancestors = []; while (ancestor) { ancestors.push(ancestor); ancestor = ancestor.parentElement; } for (const candidate of ancestors) { if (elements.every((el) => candidate.contains(el))) return candidate; } return document.body; } matchesPattern(text, patterns) { return patterns.some((p) => text.includes(p)); } } function makeDraggableByHeader(element) { let isDragging = false; let startX, startY, initialX, initialY; const header = element.querySelector(".mdx-toolbar-header"); if (!header) return; header.addEventListener("mousedown", (e) => { if (e.target.closest(".mdx-close-btn")) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; element.style.transition = "none"; header.style.cursor = "grabbing"; }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; element.style.left = `${initialX + deltaX}px`; element.style.top = `${initialY + deltaY}px`; element.style.right = "auto"; }); document.addEventListener("mouseup", () => { if (isDragging) { isDragging = false; element.style.transition = ""; if (header) header.style.cursor = "grab"; } }); } function makeModalDraggable(element) { let isDragging = false; let startX, startY, initialX, initialY; const header = element.querySelector(".mdx-preview-header"); if (!header) return; header.addEventListener("mousedown", (e) => { if (e.target.closest(".mdx-preview-close")) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; element.style.transition = "none"; element.style.transform = "none"; header.style.cursor = "grabbing"; }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; element.style.left = `${initialX + deltaX}px`; element.style.top = `${initialY + deltaY}px`; }); document.addEventListener("mouseup", () => { if (isDragging) { isDragging = false; element.style.transition = ""; if (header) header.style.cursor = "grab"; } }); } function escapeHtml(text) { return String(text).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } class MarkdownPreviewModal { constructor() { this.modal = null; this.markdownOptions = { includeImages: true, preserveTables: true, keepCodeFormatting: true, simplifyLayout: false, preserveLinks: true, addSeparators: true, includeXPath: false, textOnly: false }; this.onGenerateMarkdown = null; this.currentMarkdown = ""; } show(generateMarkdownCallback) { this.onGenerateMarkdown = generateMarkdownCallback; if (!this.modal) this.createModal(); this.updateContent(); this.modal.style.display = "flex"; } hide() { if (this.modal) this.modal.style.display = "none"; } createModal() { this.modal = document.createElement("div"); this.modal.className = "mdx-preview"; this.modal.innerHTML = `
${escapeHtml(this.currentMarkdown)}`;
const previewPane = this.modal.querySelector('[data-pane="preview"]');
if (window.marked && window.marked.parse) {
window.marked.setOptions && window.marked.setOptions({
gfm: true,
breaks: true,
tables: true,
headerIds: false,
mangle: false
});
const html = window.marked.parse(this.currentMarkdown);
previewPane.innerHTML = `${escapeHtml(this.currentMarkdown)}