element:", node.outerHTML);
const codeText = node.textContent.replace(/^\s+|\s+$/g, "");
result += `\`\`\`\n${codeText}\n\`\`\`\n\n`;
}
}
break;
case "code":
{
const codeText = node.textContent;
result += ` \`${codeText}\` `;
}
break;
case "hr":
if (node.getAttribute("id") !== "hr-toc") {
result += `---\n\n`;
}
break;
case "br":
result += ` \n`;
break;
case "table":
result += (await processTable(node)) + "\n\n";
break;
// case 'iframe':
// {
// const src = node.getAttribute('src') || '';
// const iframeHTML = node.outerHTML.replace('>', ' style="width: 100%; aspect-ratio: 2;">'); // Ensure proper closing
// result += `${iframeHTML}\n\n`;
// }
// break;
case "div":
{
const className = node.getAttribute("class") || "";
if (className.includes("csdn-video-box")) {
// Handle video boxes or other specific divs
// result += `${processChildren(node, listLevel)}\n\n`;
// 不递归处理了,直接在这里进行解析
const iframe = node.querySelector("iframe");
const src = iframe.getAttribute("src") || "";
const title = node.querySelector("p").textContent || "";
const iframeHTML = iframe.outerHTML.replace(
">",
' style="width: 100%; aspect-ratio: 2;">'
); // Ensure video box is full width
result += ` \n\n`;
} else if (className.includes("toc")) {
const customTitle = node.querySelector("h4").textContent || "";
if (enableTOC) {
result += `**${customTitle}**\n\n[TOC]\n\n`;
}
} else {
// result += await processChildren(node, listLevel);
result += `${await processChildren(node, listLevel)}\n`;
}
}
break;
case "span":
{
const node_class = node.getAttribute("class");
if (node_class) {
if (node_class.includes("katex--inline")) {
// class="katex-mathml"
const mathml = clearSpecialChars(
node.querySelector(".katex-mathml").textContent
);
const katex_html = clearSpecialChars(
node.querySelector(".katex-html").textContent
);
// result += ` $${mathml.replace(katex_html, "")}$ `;
if (mathml.startsWith(katex_html)) {
result += ` $${mathml.replace(katex_html, "")}$ `;
} else {
// 字符串切片,去掉 mathml 开头等同长度的 katex_html,注意不能用 replace,因为 katex_html 里的字符顺序可能会变
result += ` $${mathml.slice(katex_html.length)}$ `;
}
break;
} else if (node_class.includes("katex--display")) {
const mathml = clearSpecialChars(
node.querySelector(".katex-mathml").textContent
);
const katex_html = clearSpecialChars(
node.querySelector(".katex-html").textContent
);
// result += `$$\n${mathml.replace(katex_html, "")}\n$$\n\n`;
if (mathml.startsWith(katex_html)) {
result += `$$\n${mathml.replace(katex_html, "")}\n$$\n\n`;
} else {
// 字符串切片,去掉 mathml 开头等同长度的 katex_html,注意不能用 replace,因为 katex_html 里的字符顺序可能会变
result += `$$\n${mathml.slice(katex_html.length)}\n$$\n\n`;
}
break;
}
}
const style = node.getAttribute("style") || "";
if (
(style.includes("background-color") || style.includes("color")) &&
GM_getValue("enableColorText")
) {
result += `${await processChildren(node, listLevel)}`;
} else {
result += await processChildren(node, listLevel);
}
}
break;
case "kbd":
result += ` ${node.textContent} `;
break;
case "mark":
result += ` ${await processChildren(node, listLevel)} `;
break;
case "sub":
result += `${await processChildren(node, listLevel)}`;
break;
case "sup":
{
const node_class = node.getAttribute("class");
if (node_class && node_class.includes("footnote-ref")) {
result += `[^${node.textContent}]`;
} else {
result += `${await processChildren(node, listLevel)}`;
}
}
break;
case "svg":
{
const style = node.getAttribute("style");
if (style && style.includes("display: none")) {
break;
}
// 必须为 foreignObject 里的 div 添加属性 xmlns="http://www.w3.org/1999/xhtml" ,否则 typora 无法识别
const foreignObjects = node.querySelectorAll("foreignObject");
for (const foreignObject of foreignObjects) {
const divs = foreignObject.querySelectorAll("div");
divs.forEach((div) => {
div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
});
}
// 检查是否有 style 标签存在于 svg 元素内,如果有,则需要将 svg 元素转换为 img 元素,用 Base64 编码的方式显示。否则直接返回 svg 元素
if (node.querySelector("style")) {
const base64 = svgToBase64(node.outerHTML);
// result += `
`;
result += `\n\n`;
} else {
result += `${node.outerHTML}\n\n`;
}
}
break;
case "section": // 这个是注脚的内容
{
const node_class = node.getAttribute("class");
if (node_class && node_class.includes("footnotes")) {
result += await processFootnotes(node);
}
}
break;
case "input":
// 仅处理 checkbox 类型的 input 元素
if (node.getAttribute("type") === "checkbox") {
result += `[${node.checked ? "x" : " "}] `;
}
break;
case "dl":
// 自定义列表,懒得解析了,直接用 html 吧
result += `${shrinkHtml(node.outerHTML)}\n\n`;
break;
case "abbr":
result += `${shrinkHtml(node.outerHTML)}`;
break;
default:
result += await processChildren(node, listLevel);
result += "\n\n";
break;
}
break;
case TEXT_NODE:
result += escapeMarkdown(node.textContent);
break;
case COMMENT_NODE:
// Ignore comments
break;
default:
break;
}
return result;
}
/**
* 处理给定节点的子节点。
* @param {Node} node - 父节点。
* @param {number} listLevel - 当前列表嵌套级别。
* @returns {Promise} - 子节点拼接后的 Markdown 字符串。
*/
async function processChildren(node, listLevel) {
let text = "";
for (const child of node.childNodes) {
text += await processNode(child, listLevel);
}
return text;
}
/**
* 处理列表元素 ( 或 )。
* @param {Element} node - 列表元素。
* @param {number} listLevel - 当前列表嵌套级别。
* @param {boolean} ordered - 列表是否有序。
* @returns {Promise} - 列表的 Markdown 字符串。
*/
async function processList(node, listLevel, ordered) {
let text = "";
const children = Array.from(node.children).filter((child) => child.tagName.toLowerCase() === "li");
text += "\n";
for (let index = 0; index < children.length; index++) {
const child = children[index];
let prefix = ordered ? `${" ".repeat(listLevel)}${index + 1}. ` : `${" ".repeat(listLevel)}- `;
const childText = (await processChildren(child, listLevel + 1)).trim();
text += `${prefix}${childText}\n`;
}
text += `\n`;
return text;
}
/**
* 处理表格。
* @param {Element} node - 包含表格的元素。
* @returns {Promise} - 表格的 Markdown 字符串。
*/
async function processTable(node) {
const rows = Array.from(node.querySelectorAll("tr"));
if (rows.length === 0) return "";
let table = "";
// Process header
const headerCells = Array.from(rows[0].querySelectorAll("th, td"));
const headers = await Promise.all(headerCells.map(async (cell) => (await processNode(cell)).trim()));
table += `| ${headers.join(" | ")} |\n`;
// Process separator
const alignments = headerCells.map((cell) => {
const align = cell.getAttribute("align");
if (align === "center") {
return ":---:";
} else if (align === "right") {
return "---:";
} else if (align === "left") {
return ":---";
} else {
return ":---:";
}
});
table += `|${alignments.join("|")}|\n`;
// Process body
for (let i = 1; i < rows.length; i++) {
const cells = Array.from(rows[i].querySelectorAll("td"));
const row = await Promise.all(cells.map(async (cell) => (await processNode(cell)).trim()));
table += `| ${row.join(" | ")} |\n`;
}
return table;
}
/**
* 处理代码块。有两种代码块,一种是老版本的代码块,一种是新版本的代码块。
* @param {Element} node - 包含代码块的元素。一般是 元素。
* @returns {Promise} - 代码块的 Markdown 字符串。
*/
async function processCodeBlock(codeNode) {
// 查找 code 内部是否有 ol 元素,这两个是老/新版本的代码块,需要分开处理
const node = codeNode.querySelector("ol");
// 确保传入的节点是一个 元素
if (!node || node.tagName.toLowerCase() !== "ol") {
// console.error('Invalid node: Expected an element.');
// return '';
// 如果没有 ol 元素,则说明是老版本,直接返回 codeNode 的 textContent
// return codeNode.textContent + '\n';
// 如果尾部有换行符,则去掉
return codeNode.textContent.replace(/\n$/, "") + "\n";
}
// 获取所有 - 子元素
const listItems = node.querySelectorAll("li");
let result = "";
// 遍历每个
- 元素
listItems.forEach((li, index) => {
// 将
- 的 textContent 添加到结果中
result += li.textContent;
result += "\n";
});
return result;
}
/**
* 处理脚注。
* @param {Element} node - 包含脚注的元素。
* @returns {Promise
} - 脚注的 Markdown 字符串。
*/
async function processFootnotes(node) {
const footnotes = Array.from(node.querySelectorAll("li"));
let result = "";
for (let index = 0; index < footnotes.length; index++) {
const li = footnotes[index];
const text = (await processNode(li)).replaceAll("\n", " ").replaceAll("↩︎", "").trim();
result += `[^${index + 1}]: ${text}\n`;
}
return result;
}
let markdown = "";
for (const child of articleElement.childNodes) {
markdown += await processNode(child);
}
// markdown = markdown.replace(/[\n]{3,}/g, '\n\n');
return markdown.trim();
}
/**
* 下载文章内容并转换为 Markdown 格式。并保存为文件。这里会额外获取文章标题和文章信息并添加到 Markdown 文件的开头。
* @param {Document} doc_body - 文章的 body 元素。
* @returns {Promise} - 下载完成后的 Promise 对象。
*/
async function downloadCSDNArticleToMarkdown(doc_body, getZip = false, url = "", prefix = "") {
const articleTitle = doc_body.querySelector("#articleContentId")?.textContent.trim() || "未命名文章";
const articleInfo = doc_body.querySelector(".bar-content")?.textContent.replace(/\s{2,}/g, " ").trim() || "";
const htmlInput = doc_body.querySelector("#content_views");
if (!htmlInput) {
alert("未找到文章内容。");
return;
}
let mode = GM_getValue("parallelDownload") ? "并行" : "串行";
mode += GM_getValue("fastDownload") ? "快速" : "完整";
showFloatTip(`正在以${mode}模式下载文章:` + articleTitle);
if (url === "") {
url = window.location.href;
}
// url = url.replace(/[?#@!$&'()*+,;=].*$/, "");
url = clearUrl(url);
let markdown = await htmlToMarkdown(htmlInput, GM_getValue("mergeArticleContent") ? "assets" : `${prefix}${articleTitle}`, !GM_getValue("mergeArticleContent"));
if (GM_getValue("addArticleInfoInBlockquote")) {
markdown = `> ${articleInfo}\n> 文章链接:${url}\n\n${markdown}`;
}
if (GM_getValue("addArticleTitleToMarkdown")) {
if (GM_getValue("addSerialNumberToTitle")) {
markdown = `# ${prefix}${articleTitle}\n\n${markdown}`;
} else {
markdown = `# ${articleTitle}\n\n${markdown}`;
}
}
if (GM_getValue("addArticleInfoInYaml")) {
const article_info_box = doc_body.querySelector(".article-info-box");
// 文章标题
const meta_title = GM_getValue("addSerialNumberToTitle") ? `${prefix}${articleTitle}` : articleTitle;
// 文字文字 YYYY-MM-DD HH:MM:SS 文字文字
const meta_date =
article_info_box.querySelector(".time")?.textContent.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)[0] ||
"";
let articleMeta = `title: ${meta_title}\ndate: ${meta_date}\n`;
// 文章分类
const meta_category_and_tags = Array.from(article_info_box.querySelectorAll(".tag-link")) || [];
if (meta_category_and_tags.length > 0 && article_info_box.textContent.includes("分类专栏")) {
articleMeta += `categories:\n- ${meta_category_and_tags[0].textContent}\n`;
meta_category_and_tags.shift();
}
if (meta_category_and_tags.length > 0 && article_info_box.textContent.includes("文章标签")) {
articleMeta += `tags:\n${Array.from(meta_category_and_tags)
.map((tag) => `- ${tag.textContent}`)
.join("\n")}\n`;
}
markdown = `---\n${articleMeta}---\n\n${markdown}`;
}
// markdown = `# ${articleTitle}\n\n> ${articleInfo}\n\n${markdown}`;
// 从 prefix 中获取序号
const index = parseInt(prefix.match(/\d+/)[0]);
await saveTextAsFile(markdown, `${prefix}${articleTitle}.md`, index);
if (getZip) {
await saveAllFileToZip(`${prefix}${articleTitle}`);
}
}
/**
* 创建一个隐藏的 iframe 并下载指定 URL 的文章。
* @param {string} url - 文章的 URL。
* @returns {Promise} - 下载完成后的 Promise 对象。
*/
async function downloadArticleInIframe(url, prefix = "") {
return new Promise((resolve, reject) => {
// 创建一个隐藏的 iframe
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.src = url;
document.body.appendChild(iframe);
// 监听 iframe 加载完成事件
iframe.onload = async () => {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
// 调用下载函数
await downloadCSDNArticleToMarkdown(doc.body, false, url, prefix);
// 移除 iframe
document.body.removeChild(iframe);
resolve();
} catch (error) {
// 在发生错误时移除 iframe 并拒绝 Promise
document.body.removeChild(iframe);
console.error("下载文章时出错:", error);
reject(error);
}
};
// 监听 iframe 加载错误事件
iframe.onerror = async () => {
document.body.removeChild(iframe);
console.error("无法加载文章页面:", url);
reject(new Error("无法加载文章页面"));
};
});
}
async function downloadArticleFromBatchURL(url, prefix = "") {
if (!GM_getValue("addSerialNumber")) {
prefix = "";
}
if (GM_getValue("fastDownload")) {
const response = await fetch(url);
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
// 调用下载函数
await downloadCSDNArticleToMarkdown(doc.body, false, url, prefix);
} else {
await downloadArticleInIframe(url, prefix);
}
}
/**
* 下载专栏的全部文章为 Markdown 格式。
* @returns {Promise} - 下载完成后的 Promise 对象。
*/
async function downloadCSDNCategoryToMarkdown() {
// 获取专栏 id,注意 url 可能是 /category_数字.html 或 /category_数字_数字.html,需要第一个数字
showFloatTip("正在获取专栏的全部文章链接...");
const base_url = window.location.href;
const category_id = base_url.match(/category_(\d+)(?:_\d+)?\.html/)[1];
const url_list = [];
let page = 1;
let doc_body = document.body;
while (true) {
let hasNextArticle = false;
// 获取当前页面的文章列表
doc_body
.querySelector(".column_article_list")
.querySelectorAll("a")
.forEach((item) => {
url_list.push(item.href);
hasNextArticle = true;
});
if (!hasNextArticle) break;
// 下一页
page++;
const next_url = base_url.replace(/category_\d+(?:_\d+)?\.html/, `category_${category_id}_${page}.html`);
const response = await fetch(next_url);
const text = await response.text();
const parser = new DOMParser();
doc_body = parser.parseFromString(text, "text/html").body;
}
if (url_list.length === 0) {
showFloatTip("没有找到文章。");
return;
} else {
showFloatTip(
`找到 ${url_list.length} 篇文章。开始下载...(预计时间:${Math.round(url_list.length * 0.6)} 秒)`
);
}
// 下载每篇文章
const prefixMaxLength = url_list.length.toString().length;
if (GM_getValue("parallelDownload")) {
await Promise.all(
url_list.map((url, index) =>
downloadArticleFromBatchURL(
url,
`${String(url_list.length - index).padStart(prefixMaxLength, "0")}_`
)
)
);
} else {
for (let i = 0; i < url_list.length; i++) {
await downloadArticleFromBatchURL(
url_list[i],
`${String(url_list.length - i).padStart(prefixMaxLength, "0")}_`
);
}
}
let extraPrefix = "";
if (GM_getValue("addArticleTitleToMarkdown")) {
extraPrefix += `# ${document.title}\n\n`
}
if (GM_getValue("addArticleInfoInBlockquote_batch")) {
const batchTitle = document.body.querySelector(".column_title")?.textContent.trim() || "";
const batchDesc = document.body.querySelector(".column_text_desc")?.textContent.trim() || "";
const batchColumnData = document.body.querySelector(".column_data")?.textContent.replace(/\s{2,}/g, " ").trim() || "";
const batchAuthor = document.body.querySelector(".column_person_tit")?.textContent.replace(/\s{2,}/g, " ").trim() || "";
const batchUrl = clearUrl(base_url);
extraPrefix += `> ${batchDesc}\n> ${batchAuthor} ${batchColumnData}\n${batchUrl}\n\n`
}
if (GM_getValue("mergeArticleContent")) {
mergeArticleContent(`${document.title}`, extraPrefix);
}
if (GM_getValue("zipCategories")) {
await saveAllFileToZip(`${document.title}`);
showFloatTip(
`专栏文章全部处理完毕,请等待打包。(预计时间: ${Math.round(url_list.length * 0.25)} 秒)`,
url_list.length * 250
);
} else {
if (GM_getValue("mergeArticleContent")) {
downloadMergedArticle();
}
showFloatTip("专栏文章全部处理完毕,请等待下载结束。", 3000);
}
}
/**
* 下载用户的全部文章为 Markdown 格式。
* @returns {Promise} - 下载完成后的 Promise 对象。
*/
async function downloadAllArticlesOfUserToMarkdown() {
showFloatTip("正在获取用户全部文章链接。可能需要进行多次页面滚动,请耐心等待。");
const mainContent = document.body.querySelector(".mainContent");
const url_list = [];
const url_set = new Set();
while (true) {
// 等待 2 秒,等待页面加载完成
await new Promise((resolve) => setTimeout(resolve, 2000));
window.scrollTo({
top: document.body.scrollHeight,
behavior: "smooth", // 可选,使滚动平滑
});
let end = true;
mainContent.querySelectorAll("article").forEach((item) => {
const url = item.querySelector("a").href;
if (!url_set.has(url)) {
url_list.push(url);
url_set.add(url);
end = false;
}
});
if (end) break;
}
// 滚回顶部
window.scrollTo({
top: 0,
behavior: "smooth", // 可选,使滚动平滑
});
if (url_list.length === 0) {
showFloatTip("没有找到文章。");
} else {
showFloatTip(
`找到 ${url_list.length} 篇文章。开始下载...(预计时间:${Math.round(url_list.length * 0.6)} 秒)`
);
}
// 下载每篇文章
const prefixMaxLength = url_list.length.toString().length;
if (GM_getValue("parallelDownload")) {
await Promise.all(
url_list.map((url, index) =>
downloadArticleFromBatchURL(
url,
`${String(url_list.length - index).padStart(prefixMaxLength, "0")}_`
)
)
);
} else {
for (let i = 0; i < url_list.length; i++) {
await downloadArticleFromBatchURL(
url_list[i],
`${String(url_list.length - i).padStart(prefixMaxLength, "0")}_`
);
}
}
let extraPrefix = "";
if (GM_getValue("addArticleTitleToMarkdown")) {
extraPrefix += `# ${document.title}\n\n`
}
if (GM_getValue("addArticleInfoInBlockquote_batch")) {
const batchUrl = clearUrl(window.location.href);
extraPrefix += `> ${batchUrl}\n\n`
}
if (GM_getValue("mergeArticleContent")) {
mergeArticleContent(`${document.title}`, extraPrefix);
}
if (GM_getValue("zipCategories")) {
await saveAllFileToZip(`${document.title}`);
showFloatTip(
`用户全部文章处理完毕,请等待打包。(预计时间: ${Math.round(url_list.length * 0.25)} 秒)`,
url_list.length * 250
);
} else {
if (GM_getValue("mergeArticleContent")) {
downloadMergedArticle();
}
showFloatTip("用户全部文章处理完毕,请等待下载结束。", 3000);
}
}
/**
* 主函数。点击下载按钮后执行。
* @returns {Promise} - 运行完成后的 Promise
*/
async function runMain() {
// 检查是专栏还是文章
// 专栏的 url 里有 category
// 文章的 url 里有 article/details
disableFloatWindow();
const url = window.location.href;
if (url.includes("category")) {
// 专栏
await downloadCSDNCategoryToMarkdown();
} else if (url.includes("article/details")) {
// 文章
if (GM_getValue("mergeArticleContent")) {
GM_setValue("mergeArticleContent", false);
await downloadCSDNArticleToMarkdown(document.body, GM_getValue("zipCategories"), window.location.href);
GM_setValue("mergeArticleContent", true);
} else {
await downloadCSDNArticleToMarkdown(document.body, GM_getValue("zipCategories"), window.location.href);
}
showFloatTip("文章下载完成。", 3000);
} else if (url.includes("type=blog")) {
await downloadAllArticlesOfUserToMarkdown();
} else {
alert("无法识别的页面。");
}
enableFloatWindow();
saveWebImageToLocal(null, null, true);
}
})();