Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save robertovg/c6ed0ef05a3d1d4c958f551c2abb54c8 to your computer and use it in GitHub Desktop.

Select an option

Save robertovg/c6ed0ef05a3d1d4c958f551c2abb54c8 to your computer and use it in GitHub Desktop.
Full Site to Markdown Extractor (Bookmarklet)
/**
* # Full Site to Markdown Extractor (Bookmarklet)
*
* A browser bookmarklet that converts the current page — and optionally other same-origin pages you pick — into clean Markdown, copied straight to your clipboard.
*
* ## Features
*
* - Converts headings, paragraphs, lists (nested), blockquotes, code blocks, tables, and links to Markdown.
* - Lets you pick additional same-origin pages (via a link-selection modal) to bundle into one combined Markdown document.
* - Optional toggles: include images, include inline content links, include links on page section headings, include a "pages selected" index.
* - Special-cased extraction for Jira issue pages (Cloud & Data Center), pulling the rich-text description out of the Atlassian editor renderer.
* - Strips noise elements (scripts, nav, ads, cookie banners, etc.) before conversion.
* - Falls back to a popup window with the raw Markdown if clipboard access is unavailable.
*
* ## Usage
*
* 1.- Save the script as a bookmarklet (wrap in javascript:(...)() and minify, or use a bookmarklet-builder).
* 2.- Click it on any page.
* 3.- Optionally select related same-origin pages to include, choose export options, then click "Export Markdown".
* 4.- Paste the result — Markdown is on your clipboard.
*
* ## Notes
*
* - Cross-page fetches only ever target the same origin as the current page (uses your existing session).
* - No data is sent anywhere external — everything stays in the browser (clipboard/local popup only).
*
*/
(async () => {
const TIMEOUT_MS = 12000;
let CONTENT_HEADING_OFFSET = 2;
let exportOptions = {
includeImagesInMd: false,
includeContentLinks: false,
includeLinksOnPageSections: false,
includePagesSelectedPreSection: false,
};
const normalizeWhitespace = (text) =>
(text || "").replace(/\s+/g, " ").replace(/\u00a0/g, " ").trim();
const toAbsoluteUrl = (href, baseUrl) => {
try {
return new URL(href, baseUrl).href;
} catch {
return null;
}
};
const escapeMarkdown = (text) =>
(text || "")
.replace(/\\/g, "\\\\")
.replace(/\[/g, "\\[")
.replace(/\]/g, "\\]")
.replace(/\(/g, "\\(")
.replace(/\)/g, "\\)")
.replace(/`/g, "\\`");
const isJiraUrl = (urlString) => {
try {
const url = new URL(urlString, window.location.href);
return (
/atlassian\.net$/i.test(url.hostname) ||
/jira/i.test(url.hostname) ||
/\/browse\//i.test(url.pathname) ||
/\/jira\//i.test(url.pathname)
);
} catch {
return false;
}
};
const pickMainContainer = (doc, pageUrl) => {
if (isJiraUrl(pageUrl)) {
return (
doc.querySelector("[data-testid='issue.views.issue-base.foundation.content.content']") ||
doc.querySelector("[data-testid='issue.views.issue-base.foundation.main']") ||
doc.querySelector("[data-testid='issue-layout.ui.issue-layout']") ||
doc.querySelector(".issue-body-content") ||
doc.querySelector(".viewissue-content") ||
doc.querySelector("article") ||
doc.querySelector("main") ||
doc.querySelector("[role='main']") ||
doc.body
);
}
return (
doc.querySelector("article") ||
doc.querySelector("main") ||
doc.querySelector("[role='main']") ||
doc.body
);
};
const extractJiraDescriptionMarkdown = (doc, pageUrl) => {
const containerSelectors = [
// Jira Cloud — specific description field
"[data-testid='issue.views.field.rich-text.description']",
"[data-testid='issue.views.field.rich-text.description.content']",
"[data-testid='issue.views.issue-base.description.description']",
// Atlassian editor renderer (Cloud and Data Center)
".ak-renderer-document",
// Jira Server / Data Center
"#description-val .user-content-block",
"#description-val",
"[id*='description'][class*='value']",
];
for (const selector of containerSelectors) {
const container = doc.querySelector(selector);
if (!container) {
continue;
}
const contentRoot = container.matches(".ak-renderer-document")
? container
: container.querySelector(".ak-renderer-document") || container;
// Process direct children of the container to preserve block structure.
// Atlassian renderer may use <div>, <span>, <p>, or custom elements — process them all.
const children = Array.from(contentRoot.childNodes);
if (children.length === 0) {
continue;
}
const parts = children
.map((child) => {
// Skip text nodes that are only whitespace
if (child.nodeType === Node.TEXT_NODE) {
const text = normalizeWhitespace(child.nodeValue || "");
return text ? text : "";
}
// Process element nodes
return convertBlock(child, pageUrl, 0).trim();
})
.filter(Boolean);
if (parts.length > 0) {
return parts.join("\n\n");
}
}
return "";
};
const removeNoise = (root) => {
const selectors = [
"script",
"style",
"noscript",
"svg",
"canvas",
"iframe",
"nav",
"header",
"footer",
"aside",
"form",
"button",
"[aria-hidden='true']",
".ads",
".advertisement",
".cookie",
".newsletter",
];
root.querySelectorAll(selectors.join(",")).forEach((el) => el.remove());
};
const BLOCK_TAGS = new Set([
"article",
"blockquote",
"div",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"hr",
"li",
"main",
"ol",
"p",
"pre",
"section",
"table",
"ul",
]);
const isBlockElement = (node) =>
!!node && node.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has(node.tagName.toLowerCase());
const convertContainerChildren = (node, baseUrl, depth) => {
const parts = [];
let inlineBuffer = "";
const flushInlineBuffer = () => {
const text = normalizeWhitespace(inlineBuffer);
if (text) {
parts.push(`${text}\n\n`);
}
inlineBuffer = "";
};
Array.from(node.childNodes).forEach((child) => {
if (isBlockElement(child)) {
flushInlineBuffer();
parts.push(convertBlock(child, baseUrl, depth + 1));
return;
}
inlineBuffer += convertInline(child, baseUrl);
});
flushInlineBuffer();
return parts.join("");
};
const convertListItem = (node, baseUrl, depth, marker) => {
const contentParts = [];
const nestedBlocks = [];
let inlineBuffer = "";
const flushInlineBuffer = () => {
const text = normalizeWhitespace(inlineBuffer);
if (text) {
contentParts.push(text);
}
inlineBuffer = "";
};
Array.from(node.childNodes).forEach((child) => {
if (!isBlockElement(child)) {
inlineBuffer += convertInline(child, baseUrl);
return;
}
const childTag = child.tagName.toLowerCase();
if (childTag === "ul" || childTag === "ol") {
flushInlineBuffer();
nestedBlocks.push(convertBlock(child, baseUrl, depth + 1).trimEnd());
return;
}
flushInlineBuffer();
const blockText = convertBlock(child, baseUrl, depth).trim();
if (blockText) {
contentParts.push(blockText.replace(/\s*\n\s*/g, " "));
}
});
flushInlineBuffer();
const indent = " ".repeat(depth);
const text = normalizeWhitespace(contentParts.join(" "));
const line = text ? `${indent}${marker} ${text}` : `${indent}${marker}`;
const nested = nestedBlocks.filter(Boolean).join("\n");
return nested ? `${line}\n${nested}` : line;
};
const convertInline = (node, baseUrl) => {
if (!node) {
return "";
}
if (node.nodeType === Node.TEXT_NODE) {
return node.nodeValue || "";
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return "";
}
const tag = node.tagName.toLowerCase();
const children = Array.from(node.childNodes)
.map((child) => convertInline(child, baseUrl))
.join("");
if (tag === "a") {
const href = toAbsoluteUrl(node.getAttribute("href"), baseUrl) || "";
const label = normalizeWhitespace(children) || href;
if (!exportOptions.includeContentLinks) {
return label ? ` ${label} ` : "";
}
return href ? `[${escapeMarkdown(label)}](${href})` : label;
}
if (tag === "strong" || tag === "b") {
return `**${children.trim()}**`;
}
if (tag === "em" || tag === "i") {
return `*${children.trim()}*`;
}
if (tag === "code") {
return `\`${children.replace(/`/g, "\\`").trim()}\``;
}
if (tag === "img") {
if (!exportOptions.includeImagesInMd) {
return "";
}
const src = toAbsoluteUrl(node.getAttribute("src"), baseUrl);
const alt = normalizeWhitespace(node.getAttribute("alt") || "image");
return src ? `![${escapeMarkdown(alt)}](${src})` : "";
}
if (tag === "br") {
return "\n";
}
return children;
};
const convertBlock = (node, baseUrl, depth = 0) => {
if (!node) {
return "";
}
if (node.nodeType === Node.TEXT_NODE) {
return normalizeWhitespace(node.nodeValue || "");
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return "";
}
const tag = node.tagName.toLowerCase();
if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) {
const level = Math.min(6, Number(tag.slice(1)) + CONTENT_HEADING_OFFSET);
const text = normalizeWhitespace(convertInline(node, baseUrl));
return text ? `${"#".repeat(level)} ${text}\n\n` : "";
}
if (tag === "p") {
const text = normalizeWhitespace(convertInline(node, baseUrl));
return text ? `${text}\n\n` : "";
}
if (tag === "blockquote") {
const text = normalizeWhitespace(convertInline(node, baseUrl));
return text
? `${text
.split("\n")
.map((line) => `> ${line}`)
.join("\n")}\n\n`
: "";
}
if (tag === "pre") {
const code = node.textContent || "";
return code ? `\`\`\`\n${code.trim()}\n\`\`\`\n\n` : "";
}
if (tag === "ul" || tag === "ol") {
const items = Array.from(node.children).filter(
(child) => child.tagName && child.tagName.toLowerCase() === "li"
);
const lines = items
.map((li, index) => {
const marker = tag === "ol" ? `${index + 1}.` : "-";
const itemText = convertListItem(li, baseUrl, depth, marker);
if (!itemText || itemText === `${" ".repeat(depth)}${marker}`) {
return "";
}
return itemText;
})
.filter(Boolean)
.join("\n");
return lines ? `${lines}\n\n` : "";
}
if (tag === "hr") {
return "---\n\n";
}
if (tag === "table") {
const rows = Array.from(node.querySelectorAll("tr"));
if (!rows.length) {
return "";
}
const matrix = rows.map((row) =>
Array.from(row.querySelectorAll("th, td")).map((cell) =>
normalizeWhitespace(convertInline(cell, baseUrl))
)
);
const colCount = Math.max(...matrix.map((r) => r.length), 0);
if (!colCount) {
return "";
}
const header = matrix[0].map((value) => value || " ");
const separator = Array.from({ length: colCount }, () => "---");
const bodyRows = matrix.slice(1);
const asRow = (arr) =>
`| ${Array.from({ length: colCount }, (_, i) => arr[i] || " ").join(
" | "
)} |`;
return [asRow(header), asRow(separator), ...bodyRows.map(asRow)].join("\n") + "\n\n";
}
const directTextTags = ["div", "section", "article", "main"];
if (directTextTags.includes(tag)) {
const hasBlockChildren = Array.from(node.children).some((child) => isBlockElement(child));
if (!hasBlockChildren) {
const text = normalizeWhitespace(convertInline(node, baseUrl));
return text ? `${text}\n\n` : "";
}
return convertContainerChildren(node, baseUrl, depth);
}
return Array.from(node.childNodes)
.map((child) => convertBlock(child, baseUrl, depth + 1))
.join("");
};
const htmlDocumentFromString = (htmlText) =>
new DOMParser().parseFromString(htmlText, "text/html");
const fetchHtml = async (url) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const res = await fetch(url, {
method: "GET",
credentials: "include",
signal: controller.signal,
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.text();
} finally {
clearTimeout(timeout);
}
};
const extractMarkdownFromDocument = (doc, pageUrl) => {
const title = normalizeWhitespace(doc.title || pageUrl);
const description = normalizeWhitespace(
doc.querySelector("meta[name='description']")?.getAttribute("content") || ""
);
const root = pickMainContainer(doc, pageUrl).cloneNode(true);
removeNoise(root);
let markdownBody = convertBlock(root, pageUrl)
.replace(/\n{3,}/g, "\n\n")
.trim();
// Jira renders a plain "Description" label where the rich-text content lives.
// Inject the formatted description there; keep the rest of the body (subtasks, details…).
if (isJiraUrl(pageUrl)) {
const jiraDescription = extractJiraDescriptionMarkdown(doc, pageUrl);
if (jiraDescription) {
// Replace the bare "Description" line that Jira injects as a label
if (/\nDescription\n/.test(markdownBody)) {
markdownBody = markdownBody.replace(/\nDescription\n/, `\n\n${jiraDescription}\n\n`);
} else if (markdownBody.startsWith("Description\n")) {
markdownBody = markdownBody.replace(/^Description\n/, `${jiraDescription}\n\n`);
} else {
// Fallback: insert after the h1 title line
markdownBody = markdownBody.replace(/^(#[^\n]+\n\n?)/, `$1${jiraDescription}\n\n`);
}
}
}
return {
title,
description,
markdownBody,
};
};
const collectSameOriginLinks = () => {
const currentUrl = new URL(window.location.href);
const seen = new Set();
const links = [];
Array.from(document.querySelectorAll("a[href]"))
.map((a) => ({
href: toAbsoluteUrl(a.getAttribute("href"), window.location.href),
text: normalizeWhitespace(a.textContent),
}))
.forEach(({ href, text }) => {
if (!href) {
return;
}
const url = new URL(href);
if (!["http:", "https:"].includes(url.protocol)) {
return;
}
if (url.origin !== currentUrl.origin) {
return;
}
url.hash = "";
const normalizedHref = url.href;
if (normalizedHref === currentUrl.href || seen.has(normalizedHref)) {
return;
}
seen.add(normalizedHref);
links.push({
href: normalizedHref,
text: text || normalizedHref,
});
});
return links;
};
const formatPageSectionHeading = (title, url) => {
const safeTitle = escapeMarkdown(title);
if (!exportOptions.includeLinksOnPageSections) {
return `## ${safeTitle}`;
}
return `## [${safeTitle}](${url})`;
};
const openSelectionModal = (currentPageTitle, links) =>
new Promise((resolve) => {
const overlay = document.createElement("div");
overlay.style.position = "fixed";
overlay.style.inset = "0";
overlay.style.background = "rgba(0, 0, 0, 0.55)";
overlay.style.zIndex = "2147483647";
overlay.style.display = "flex";
overlay.style.alignItems = "center";
overlay.style.justifyContent = "center";
const panel = document.createElement("div");
panel.style.width = "min(920px, 92vw)";
panel.style.maxHeight = "88vh";
panel.style.overflow = "hidden";
panel.style.background = "#fff";
panel.style.color = "#111";
panel.style.borderRadius = "12px";
panel.style.padding = "16px";
panel.style.boxShadow = "0 16px 48px rgba(0, 0, 0, 0.35)";
panel.style.fontFamily = "ui-sans-serif, system-ui, sans-serif";
const title = document.createElement("h3");
title.textContent = "Select pages to export";
title.style.margin = "0 0 8px 0";
const subtitle = document.createElement("p");
subtitle.textContent = `Current page is always included: ${currentPageTitle}`;
subtitle.style.margin = "0 0 12px 0";
subtitle.style.fontSize = "14px";
subtitle.style.opacity = "0.85";
const actions = document.createElement("div");
actions.style.display = "flex";
actions.style.gap = "8px";
actions.style.flexWrap = "wrap";
actions.style.marginBottom = "10px";
const searchInput = document.createElement("input");
searchInput.type = "search";
searchInput.placeholder = "Filter pages...";
searchInput.style.flex = "1";
searchInput.style.minWidth = "260px";
searchInput.style.padding = "8px 10px";
searchInput.style.border = "1px solid #c7c7c7";
searchInput.style.borderRadius = "8px";
const selectAllBtn = document.createElement("button");
selectAllBtn.type = "button";
selectAllBtn.textContent = "Select all";
selectAllBtn.style.padding = "8px 10px";
const clearAllBtn = document.createElement("button");
clearAllBtn.type = "button";
clearAllBtn.textContent = "Clear";
clearAllBtn.style.padding = "8px 10px";
actions.appendChild(searchInput);
actions.appendChild(selectAllBtn);
actions.appendChild(clearAllBtn);
const list = document.createElement("div");
list.style.border = "1px solid #dcdcdc";
list.style.borderRadius = "8px";
list.style.padding = "8px";
list.style.maxHeight = "42vh";
list.style.overflow = "auto";
const rows = links.map((link, index) => {
const row = document.createElement("label");
row.style.display = "flex";
row.style.gap = "8px";
row.style.padding = "6px";
row.style.cursor = "pointer";
row.style.borderRadius = "6px";
row.style.alignItems = "flex-start";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.dataset.index = String(index);
const textWrap = document.createElement("span");
textWrap.style.display = "flex";
textWrap.style.flexDirection = "column";
textWrap.style.gap = "2px";
const titleEl = document.createElement("span");
titleEl.textContent = link.text;
titleEl.style.fontSize = "14px";
const urlEl = document.createElement("span");
urlEl.textContent = link.href;
urlEl.style.fontSize = "12px";
urlEl.style.opacity = "0.75";
urlEl.style.wordBreak = "break-all";
textWrap.appendChild(titleEl);
textWrap.appendChild(urlEl);
row.appendChild(checkbox);
row.appendChild(textWrap);
list.appendChild(row);
return { row, checkbox, link };
});
if (!rows.length) {
const empty = document.createElement("div");
empty.textContent = "No same-origin links found on this page.";
empty.style.opacity = "0.75";
empty.style.fontSize = "14px";
list.appendChild(empty);
}
const optionsWrap = document.createElement("div");
optionsWrap.style.marginTop = "12px";
optionsWrap.style.display = "grid";
optionsWrap.style.gap = "6px";
const makeOption = (labelText, checked = true) => {
const label = document.createElement("label");
label.style.display = "flex";
label.style.alignItems = "center";
label.style.gap = "8px";
label.style.fontSize = "14px";
const input = document.createElement("input");
input.type = "checkbox";
input.checked = checked;
label.appendChild(input);
label.appendChild(document.createTextNode(labelText));
return { label, input };
};
const includeImagesOption = makeOption("Include images in MD", false);
const includeContentLinksOption = makeOption("Include links in page content", false);
const includeLinksOnSectionsOption = makeOption("Include links on page sections", false);
const includePagesSelectedOption = makeOption("Include pages selected pre-section", false);
optionsWrap.appendChild(includeImagesOption.label);
optionsWrap.appendChild(includeContentLinksOption.label);
optionsWrap.appendChild(includeLinksOnSectionsOption.label);
optionsWrap.appendChild(includePagesSelectedOption.label);
const footer = document.createElement("div");
footer.style.display = "flex";
footer.style.justifyContent = "space-between";
footer.style.alignItems = "center";
footer.style.marginTop = "14px";
const count = document.createElement("span");
count.style.fontSize = "13px";
count.style.opacity = "0.8";
const footerButtons = document.createElement("div");
footerButtons.style.display = "flex";
footerButtons.style.gap = "8px";
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.textContent = "Cancel";
cancelBtn.style.padding = "8px 12px";
const runBtn = document.createElement("button");
runBtn.type = "button";
runBtn.textContent = "Export Markdown";
runBtn.style.padding = "8px 12px";
runBtn.style.fontWeight = "600";
footerButtons.appendChild(cancelBtn);
footerButtons.appendChild(runBtn);
footer.appendChild(count);
footer.appendChild(footerButtons);
panel.appendChild(title);
panel.appendChild(subtitle);
panel.appendChild(actions);
panel.appendChild(list);
panel.appendChild(optionsWrap);
panel.appendChild(footer);
overlay.appendChild(panel);
document.body.appendChild(overlay);
const visibleRows = () => rows.filter((item) => item.row.style.display !== "none");
const updateCount = () => {
const selected = rows.filter((item) => item.checkbox.checked).length;
count.textContent = `${selected} selected`;
};
const close = (result) => {
document.removeEventListener("keydown", onKeyDown);
overlay.remove();
resolve(result);
};
const applyFilter = () => {
const query = normalizeWhitespace(searchInput.value).toLowerCase();
rows.forEach((item) => {
const haystack = `${item.link.text} ${item.link.href}`.toLowerCase();
item.row.style.display = !query || haystack.includes(query) ? "flex" : "none";
});
};
const onKeyDown = (event) => {
if (event.key === "Escape") {
close(null);
return;
}
if (event.key === "Enter") {
event.preventDefault();
runBtn.click();
}
};
rows.forEach((item) => {
item.checkbox.addEventListener("change", updateCount);
});
selectAllBtn.addEventListener("click", () => {
visibleRows().forEach((item) => {
item.checkbox.checked = true;
});
updateCount();
});
clearAllBtn.addEventListener("click", () => {
rows.forEach((item) => {
item.checkbox.checked = false;
});
updateCount();
});
searchInput.addEventListener("input", applyFilter);
cancelBtn.addEventListener("click", () => close(null));
runBtn.addEventListener("click", () => {
const selectedLinks = rows
.filter((item) => item.checkbox.checked)
.map((item) => item.link);
close({
selectedLinks,
options: {
includeImagesInMd: !!includeImagesOption.input.checked,
includeContentLinks: !!includeContentLinksOption.input.checked,
includeLinksOnPageSections: !!includeLinksOnSectionsOption.input.checked,
includePagesSelectedPreSection: !!includePagesSelectedOption.input.checked,
},
});
});
overlay.addEventListener("click", (event) => {
if (event.target === overlay) {
close(null);
}
});
document.addEventListener("keydown", onKeyDown);
searchInput.focus();
updateCount();
});
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
alert("Markdown copied to clipboard.");
return;
} catch {
const fallback = window.open("", "_blank", "width=800,height=600");
if (fallback) {
fallback.document.write(`<pre>${text.replace(/</g, "&lt;")}</pre>`);
} else {
alert("Could not copy automatically. Please copy from the console output.");
}
console.log(text);
}
};
const currentPageTitle = normalizeWhitespace(document.title || window.location.href);
const candidateLinks = collectSameOriginLinks();
const siteTitle = normalizeWhitespace(
document.querySelector("meta[property='og:site_name']")?.getAttribute("content") ||
window.location.hostname
);
const siteUrl = window.location.origin;
const selectionResult = await openSelectionModal(currentPageTitle, candidateLinks);
if (!selectionResult) {
return;
}
const selectedLinks = selectionResult.selectedLinks;
exportOptions = {
...exportOptions,
...(selectionResult.options || {}),
};
const isSinglePage = selectedLinks.length === 0;
if (isSinglePage) {
CONTENT_HEADING_OFFSET = 1;
}
const currentPage = extractMarkdownFromDocument(document, window.location.href);
const sections = [];
if (isSinglePage) {
sections.push(`# [${escapeMarkdown(currentPage.title)}](${window.location.href})`);
sections.push("");
if (currentPage.description) {
sections.push(currentPage.description);
sections.push("");
}
sections.push(currentPage.markdownBody || "No extractable content.");
} else {
sections.push(`# [${escapeMarkdown(siteTitle)}](${siteUrl})`);
sections.push("");
sections.push(currentPage.description || "No meta description.");
sections.push("");
if (exportOptions.includePagesSelectedPreSection !== false) {
sections.push("## Pages selected");
sections.push(`- [${escapeMarkdown(currentPage.title)}](${window.location.href})`);
for (const link of selectedLinks) {
sections.push(`- [${escapeMarkdown(link.text)}](${link.href})`);
}
sections.push("");
}
sections.push(formatPageSectionHeading(currentPage.title, window.location.href));
sections.push(currentPage.markdownBody || "No extractable content.");
for (const link of selectedLinks) {
try {
const htmlText = await fetchHtml(link.href);
const doc = htmlDocumentFromString(htmlText);
const extracted = extractMarkdownFromDocument(doc, link.href);
sections.push("");
sections.push(formatPageSectionHeading(extracted.title, link.href));
sections.push(extracted.markdownBody || "No extractable content.");
} catch (error) {
sections.push("");
sections.push(formatPageSectionHeading(link.text, link.href));
sections.push(`_Could not extract this page: ${normalizeWhitespace(error.message)}_`);
}
}
}
const markdownOutput = sections.join("\n").replace(/\n{3,}/g, "\n\n").trim();
await copyToClipboard(markdownOutput);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment