[js] 让思源笔记成为知乎、微信公众号、Github README 最佳写作软件

在支持 Markdown 的本地笔记软件里,思源笔记的笔记编辑是数一数二的:富文本编辑、悬浮工具栏、可调整字号颜色、图片宽度可以直接调整右键可以用默认程序编辑图片

并且思源笔记本身就有一个预览模式,可以一键笔记到知乎、微信公众号以及复制带图床链接的 Markdown。

PixPin_2025-07-09_11-52-46

但是目前的预览模式复制到知乎、微信公众号、Github 都有一点问题

因此我写了一个代码片段进行改进。

改进之后,对我而言,思源笔记已成为知乎、微信公众号、Github README 最佳写作软件

等后面有时间,我会给思源笔记提交 pr,对原生功能进行改进,或者官方什么时候有空,自己改吧!

1 主要改进点

PixPin_2025-07-09_11-58-22

1.1 图床优化

  • 支持使用 picgo 插件的图床链接来替换本地图片链接:官方的预览模式只支持官方图床,需要开会员,代码片段支持自动使用 picgo 插件的图床进行替换图片链接,保证本地笔记是本地图片,发布时才替换为图床链接,方便离线预览笔记、编辑图片

    这样只需要安装 picgo 插件,配置好自己的图床,就能一键粘贴图文到指定网站

    PixPin_2025-07-09_11-54-21

    PixPin_2025-07-09_11-55-53

1.2 知乎

1.2.1 图床优化

1.2.2 支持标题自动编号

1.2.3 列表格式优化

知乎编辑器的问题:

由于知乎不支持列表放图片和代码

  • 放图片的有序列表会被拆分,原本序号为 2 的列表,却变为 1
  • 列表下放代码块会变为纯文本

我的解决思路:

把有序列表直接改为普通文本

有序列表按 1、1)、A、a、i、1 的顺序循环编号,无序列表则按'✦', '○', '✧', '⟐', '⬧', '⬦'的顺序循环编号

1.2.4 支持直接粘贴带图片标题的图片

思源笔记图片格式

<span contenteditable="false" data-type="img" class="img">
  <span> </span>
  <span>
    <span class="protyle-action protyle-icons">
      <span class="protyle-icon protyle-icon--only protyle-custom cst-copy-png" style="border-top-right-radius:0;border-bottom-right-radius:0">
        <svg class="svg">
          <use xlink:href="#iconCopy"></use>
        </svg>
      </span>
      <span class="protyle-icon protyle-icon--only" style="border-top-left-radius: 0px; border-bottom-left-radius: 0px;">
        <svg class="svg">
          <use xlink:href="#iconMore"></use>
        </svg>
      </span>
    </span>
    <img src="assets/PixPin_2025-07-04_15-39-16-20250704153925-nh8u6hy.png"
         data-src="assets/PixPin_2025-07-04_15-39-16-20250704153925-nh8u6hy.png"
         alt="PixPin_2025-07-04_15-39-16"
         title="测试">
    <span class="protyle-action__drag"></span>
    <span class="protyle-action__title">
      <span>测试</span>
    </span>
  </span>
  <span> </span>
</span>

需要转换为知乎格式

<figure>
  <img src="知乎上传后的图片链接" alt="caption">
  <figcaption>caption</figcaption>
</figure>

1.2.5 粘贴代码块直接高亮

思源笔记格式

<pre class="code-block" data-language="html">
  <code class="hljs" data-render="true" style="white-space: pre-wrap; word-break: break-word; font-variant-ligatures: none;">
    <span class="hljs-tag"><<span class="hljs-name">pre</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"python"</span>></span>
    print("hello")
    <span class="hljs-tag"></<span class="hljs-name">pre</span>></span>
  </code>
</pre>

知乎格式

<pre lang="python">
print("hello")
</pre>

1.3 微信公众号

1.4 复制 Markdown

代码片段代码

// Version: 3.0
// Author: Achuan-2
// 
// - 20250709 v3.0
//   - 粘贴到知乎,支持图片标题
//   - 改进复制到知乎的代码块高亮,粘贴支持直接高亮
// - 20250630 v2.0
//   - 支持选择图床,选择默认/picgo图床,默认是默认图床,即使用思源笔记图床,使用默认图床,对于微信和知乎,不需要处理,对于Github添加 DEFAULT_IMAGE_PREFIX
//   - 改进获取微信文章链接方法,如果该笔记没有微信文章链接,但是有其他平台链接,则用其他平台链接
//   - 将Github/语雀相关的按钮和处理函数重命名为Markdown
// - 20250629 v1.0
//   - 完善知乎多级列表,对普通多级列表(没有图片和代码块)也处理为普通文本
//   - 添加标题编号功能,导出到微信公众号、知乎、Markdown都支持添加标题编号


(() => {

    // 常量定义
    const CONSTANTS = {
        // 笔记引用转微信链接
        DATABASE_AV_ID: "20230804021554-h0l44hz",
        WEIXIN_KEY_ID: "20250310104930-o0812vp",
        // 分割线转图片
        SEPARATOR_IMAGE_URL: "https://i0.hdslb.com/bfs/article/4aa545dccf7de8d4a93c2b2b8e3265ac0a26d216.png",
        // 默认图片前缀(当没有picgo图床映射时使用)
        DEFAULT_IMAGE_PREFIX: "https://assets.b3logfile.com/siyuan/1610205759005/",
        // 图床类型
        IMAGE_HOST_TYPE: {
            DEFAULT: "default",
            PICGO: "picgo"
        },
        // 微信公众号卡片
        WECHAT_PROFILE: {
            nickname: "Achuan同学",
            alias: "achuan-2-0713",
            headimg: "http://mmbiz.qpic.cn/mmbiz_png/Xh9XDwqibetTAnPRk6Z89m8u4nibAvLwuIm7icHFHtOklSNqoibTaurdMzWJojPoJzcbcJcySOJaEziavibfibOfY7DiaQ/0?wx_fmt=png",
            signature: "一条没有故事的巛,记录自己的学习感悟和笔记。",
            id: "MzU3ODg2NTc3MA==",
            introduction: "这里是Achuan同学,分享自己的学习笔记和生活随笔,欢迎点赞评论转发我的文章"
        },
        // 知乎自定义多级列表符号
        LIST_SYMBOLS: {
            UNORDERED: ['✦', '○', '✧', '⟐', '⬧', '⬦'],
            ROMAN_NUMERALS: [
                { value: 1000, symbol: 'm' },
                { value: 900, symbol: 'cm' },
                { value: 500, symbol: 'd' },
                { value: 400, symbol: 'cd' },
                { value: 100, symbol: 'c' },
                { value: 90, symbol: 'xc' },
                { value: 50, symbol: 'l' },
                { value: 40, symbol: 'xl' },
                { value: 10, symbol: 'x' },
                { value: 9, symbol: 'ix' },
                { value: 5, symbol: 'v' },
                { value: 4, symbol: 'iv' },
                { value: 1, symbol: 'i' }
            ]
        },
        // 标题编号配置
        HEADING_NUMBER: {
            ENABLED: true,
            START_LEVEL: 1, // 从H1开始编号
            END_LEVEL: 6,   // 到H6结束编号
            SEPARATOR: '.'  // 编号分隔符
        }
    };

    // 工具函数类
    class Utils {
        /**
         * 转义正则表达式特殊字符
         */
        static escapeRegExp(string) {
            return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        }

        /**
         * 执行POST请求
         */
        static async fetchSyncPost(url, data, returnType = 'json') {
            const init = {
                method: "POST",
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(data)
            };

            try {
                const res = await fetch(url, init);
                return returnType === 'json' ? await res.json() : await res.text();
            } catch (error) {
                console.error(`请求失败 ${url}:`, error);
                return returnType === 'json'
                    ? { code: error.code || 1, msg: error.message || "", data: null }
                    : "";
            }
        }

        /**
         * 复制到剪贴板
         */
        static async copyToClipboard(text) {
            try {
                if (navigator.clipboard && window.isSecureContext) {
                    await navigator.clipboard.writeText(text);
                } else {
                    const textArea = document.createElement('textarea');
                    textArea.value = text;
                    Object.assign(textArea.style, {
                        position: 'fixed',
                        left: '-999999px',
                        top: '-999999px'
                    });
                    document.body.appendChild(textArea);
                    textArea.focus();
                    textArea.select();
                    document.execCommand('copy');
                    textArea.remove();
                }
            } catch (error) {
                console.error('复制到剪贴板失败:', error);
                throw new Error('复制到剪贴板失败');
            }
        }

        /**
         * 显示通知消息
         */
        static async showNotification(message, timeout = 5000) {
            return Utils.fetchSyncPost('/api/notification/pushMsg', { msg: message, timeout });
        }

        /**
         * 获取当前文档ID
         */
        static getCurrentDocumentId() {
            const activeDocElement = document.querySelector('.protyle:not(.fn__none) .protyle-content .protyle-background[data-node-id]');
            return activeDocElement?.getAttribute('data-node-id') || null;
        }

        /**
         * 获取文档属性
         */
        static async getDocumentAttribute(docId, attributeName) {
            const data = { id: docId };
            const res = await Utils.fetchSyncPost('/api/attr/getBlockAttrs', data);
            return res?.data?.[attributeName] || null;
        }
    }

    // 数字转换工具类
    class NumberConverter {
        /**
         * 数字转字母(A-Z 或 a-z)
         */
        static numberToLetter(number, uppercase = true) {
            let result = '';
            let num = number - 1;

            do {
                result = String.fromCharCode((uppercase ? 65 : 97) + (num % 26)) + result;
                num = Math.floor(num / 26) - 1;
            } while (num >= 0);

            return result;
        }

        /**
         * 数字转罗马数字
         */
        static numberToRoman(number) {
            let result = '';
            for (const numeral of CONSTANTS.LIST_SYMBOLS.ROMAN_NUMERALS) {
                while (number >= numeral.value) {
                    result += numeral.symbol;
                    number -= numeral.value;
                }
            }
            return result;
        }
    }

    // 图片处理类
    class ImageProcessor {
        /**
         * 替换DOM中的图片URL
         */
        static async replaceImageUrlsInDOM(fileMap) {
            try {
                const typographyAreas = document.querySelectorAll('.b3-typography');

                typographyAreas.forEach(area => {
                    const images = area.querySelectorAll('img');
                    images.forEach(img => {
                        const currentSrc = img.getAttribute('src');
                        if (!currentSrc) return;

                        const fileInfo = Object.values(fileMap).find(info =>
                            info.originUrl && info.url && currentSrc === info.originUrl
                        );

                        if (fileInfo) {
                            img.setAttribute('src', fileInfo.url);
                            console.log('替换图片URL:', fileInfo.originUrl, '->', fileInfo.url);
                        }
                    });
                });
            } catch (error) {
                console.error('替换DOM中图片URL时出错:', error);
            }
        }

        /**
         * 替换markdown中的图片URL
         */
        static async replaceImageUrls(content, picgoFileMapKey) {
            try {
                const fileMap = JSON.parse(picgoFileMapKey);
                let processedContent = content;

                Object.values(fileMap).forEach(fileInfo => {
                    if (fileInfo.originUrl && fileInfo.url) {
                        const originUrl = fileInfo.originUrl;
                        const newUrl = fileInfo.url;

                        processedContent = processedContent.replace(
                            new RegExp(Utils.escapeRegExp(originUrl), 'g'),
                            newUrl
                        );

                        const imageRegex = new RegExp(
                            `!\
$$([^\$$
]*)\\]\$${Utils.escapeRegExp(originUrl)}\$`,
                            'g'
                        );
                        processedContent = processedContent.replace(imageRegex, `![$1](https://assets.b3logfile.com/siyuan/1610205759005/${newUrl})`);
                    }
                });

                return processedContent;
            } catch (error) {
                console.error('替换图片URL时出错:', error);
                return content;
            }
        }

        /**
         * 为markdown中的相对路径图片添加默认前缀
         */
        static addDefaultPrefixToImages(content) {
            try {
                // 匹配markdown图片语法中的相对路径(不以http开头的路径)
                const imageRegex = /!
$$([^$$
]*)\]$(?!https?:\/\/)([^)]+)$/g;

                return content.replace(imageRegex, (match, altText, imagePath) => {
                    // 如果路径以/开头,去掉开头的/
                    const cleanPath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;
                    const fullUrl = CONSTANTS.DEFAULT_IMAGE_PREFIX + cleanPath;
                    return `![${altText}](https://assets.b3logfile.com/siyuan/1610205759005/${fullUrl})`;
                });
            } catch (error) {
                console.error('添加默认图片前缀时出错:', error);
                return content;
            }
        }
    }

    // 链接处理类
    class LinkProcessor {
        /**
         * 从数据库获取微信链接,如果没有则获取其他平台链接
         */
        static async getWeixinLinkFromDatabase(blockId) {
            try {
                const res = await Utils.fetchSyncPost("/api/av/getAttributeViewKeys", { id: blockId });

                if (!res?.data) return null;

                const foundItem = res.data.find(item => item.avID === CONSTANTS.DATABASE_AV_ID);
                if (!foundItem?.keyValues) return null;

                const specificKey = foundItem.keyValues.find(kv => kv.key.id === CONSTANTS.WEIXIN_KEY_ID);
                if (!specificKey?.values?.length) return null;

                if (Array.isArray(specificKey.values[0].mAsset)) {
                    // 首先尝试查找微信链接
                    const weixinLink = specificKey.values[0].mAsset.find(asset =>
                        asset.content?.startsWith('https://mp.weixin.qq.com')
                    );

                    if (weixinLink) {
                        console.log('Found WeChat link for block', blockId, ':', weixinLink.content);
                        return weixinLink.content;
                    }

                    // 如果没有微信链接,尝试查找其他平台链接
                    const otherPlatformLinks = LinkProcessor.findOtherPlatformLinks(specificKey.values[0].mAsset);

                    if (otherPlatformLinks.length > 0) {
                        console.log('Found alternative platform link for block', blockId, ':', otherPlatformLinks[0].content);
                        return otherPlatformLinks[0].content;
                    }
                }

                return null;
            } catch (error) {
                console.error('Error fetching WeChat link for block:', blockId, error);
                return null;
            }
        }

        /**
         * 查找其他平台链接
         */
        static findOtherPlatformLinks(assets) {
            // 定义平台优先级(按重要性排序)
            const platformPriorities = [
                { domain: 'zhihu.com', name: '知乎' },
                { domain: 'ld246.com', name: '链滴' },
                { domain: 'juejin.cn', name: '掘金' },
                { domain: 'csdn.net', name: 'CSDN' },
                { domain: 'cnblogs.com', name: '博客园' },
                { domain: 'segmentfault.com', name: 'SegmentFault' },
                { domain: 'jianshu.com', name: '简书' },
                { domain: 'medium.com', name: 'Medium' },
                { domain: 'dev.to', name: 'Dev.to' },
                { domain: 'github.com', name: 'GitHub' },
                { domain: 'gitlab.com', name: 'GitLab' },
                { domain: 'gitee.com', name: 'Gitee' }
            ];

            const foundLinks = [];

            // 查找所有有效的链接
            assets.forEach(asset => {
                if (asset.content && asset.content.startsWith('http')) {
                    // 检查是否匹配已知平台
                    const matchedPlatform = platformPriorities.find(platform =>
                        asset.content.includes(platform.domain)
                    );

                    if (matchedPlatform) {
                        foundLinks.push({
                            content: asset.content,
                            platform: matchedPlatform.name,
                            priority: platformPriorities.indexOf(matchedPlatform)
                        });
                    } else {
                        // 对于未知平台,给较低优先级
                        foundLinks.push({
                            content: asset.content,
                            platform: '其他平台',
                            priority: 999
                        });
                    }
                }
            });

            // 按优先级排序
            foundLinks.sort((a, b) => a.priority - b.priority);

            return foundLinks;
        }

        /**
         * 转换markdown中的块链接到微信链接
         */
        static async convertBlockLinksToWeChat(content) {
            const blockLinkRegex = /
$$([^$$
]+)\]$siyuan:\/\/blocks\/([^)]+)$/g;
            let processedContent = content;
            const matches = [...content.matchAll(blockLinkRegex)];

            for (const match of matches) {
                const [fullMatch, linkText, blockId] = match;

                try {
                    const weixinLink = await LinkProcessor.getWeixinLinkFromDatabase(blockId);
                    const replacement = weixinLink
                        ? `[${linkText}](${weixinLink})`
                        : linkText;

                    processedContent = processedContent.replace(fullMatch, replacement);
                } catch (error) {
                    console.error('转换块链接时出错:', blockId, error);
                    processedContent = processedContent.replace(fullMatch, linkText);
                }
            }

            return processedContent;
        }

        /**
         * 转换所有链接到微信格式
         */
        static async convertLinksToWechat() {
            const links = document.querySelectorAll('.b3-typography a');

            for (const link of links) {
                const href = link.getAttribute('href');
                const text = link.textContent;

                if (href?.startsWith('siyuan://blocks/')) {
                    await LinkProcessor.processBlockLink(link, href, text);
                } else if (href?.startsWith('#')) {
                    LinkProcessor.replaceWithSpan(link, text, '#338dd6');
                } else if (!href?.startsWith('https://mp.weixin.qq.com/')) {
                    const newTextContent = text === href ? href : `[${text}](${href})`;
                    LinkProcessor.replaceWithSpan(link, newTextContent, '#338dd6');
                }
            }
        }

        /**
         * 转换块引用到微信格式
         */
        static async convertBlockReferencesToWechat() {
            const links = document.querySelectorAll('.b3-typography a');

            for (const link of links) {
                const href = link.getAttribute('href');
                const text = link.textContent;

                if (href?.startsWith('siyuan://blocks/')) {
                    await LinkProcessor.processBlockLink(link, href, text);
                }
            }
        }

        /**
         * 处理块链接
         */
        static async processBlockLink(link, href, text) {
            const blockId = href.replace('siyuan://blocks/', '');

            try {
                const weixinLink = await LinkProcessor.getWeixinLinkFromDatabase(blockId);

                if (weixinLink) {
                    link.setAttribute('href', weixinLink);
                } else {
                    LinkProcessor.replaceWithSpan(link, text, '#338dd6');
                }
            } catch (error) {
                console.error('Error fetching WeChat link for block:', blockId, error);
                LinkProcessor.replaceWithSpan(link, text, '#338dd6');
            }
        }

        /**
         * 用span替换链接
         */
        static replaceWithSpan(link, text, color) {
            const newSpan = document.createElement('span');
            newSpan.textContent = text;
            newSpan.style.color = color;
            link.parentNode.replaceChild(newSpan, link);
        }
    }

    // 列表处理类
    class ListProcessor {
        /**
         * 预处理有序列表以适配知乎
         */
        static processOrderedListsForZhihu() {
            const typographyAreas = document.querySelectorAll('.b3-typography');
            typographyAreas.forEach(area => {
                ListProcessor.processNestedLists(area);
            });
        }

        /**
         * 处理嵌套列表
         */
        static processNestedLists(area) {
            const orderedLists = Array.from(area.querySelectorAll('ol'));
            const unorderedLists = Array.from(area.querySelectorAll('ul'));

            // 按嵌套深度排序(深度越深越先处理)
            const allLists = [...orderedLists, ...unorderedLists]
                .sort((a, b) => ListProcessor.getListDepth(b) - ListProcessor.getListDepth(a));

            allLists.forEach(list => {
                const listDepth = ListProcessor.getListDepth(list);

                // 处理所有列表,不再限制只有包含图片和代码块的列表
                if (list.tagName === 'OL') {
                    ListProcessor.processOrderedList(list, listDepth);
                } else {
                    ListProcessor.processUnorderedList(list, listDepth);
                }
            });
        }

        /**
         * 获取列表的嵌套深度
         */
        static getListDepth(list) {
            let depth = 0;
            let parent = list.parentElement;

            while (parent) {
                if (['LI', 'OL', 'UL'].includes(parent.tagName)) {
                    if (['OL', 'UL'].includes(parent.tagName)) {
                        depth++;
                    }
                }
                parent = parent.parentElement;
            }

            return depth;
        }

        /**
         * 处理单个有序列表
         */
        static processOrderedList(ol, depth = 0) {
            const listItems = Array.from(ol.children).filter(child => child.tagName === 'LI');
            const parentElement = ol.parentNode;
            const startNumber = parseInt(ol.getAttribute('start')) || 1;

            const fragment = document.createDocumentFragment();

            listItems.forEach((li, index) => {
                const currentNumber = startNumber + index;
                const bulletPrefix = ListProcessor.getOrderedListPrefix(currentNumber, depth);
                ListProcessor.processListItem(li, bulletPrefix, fragment, index < listItems.length - 1, depth);
            });

            parentElement.insertBefore(fragment, ol);
            parentElement.removeChild(ol);
        }

        /**
         * 处理单个无序列表
         */
        static processUnorderedList(ul, depth = 0) {
            const listItems = Array.from(ul.children).filter(child => child.tagName === 'LI');
            const parentElement = ul.parentNode;

            const fragment = document.createDocumentFragment();

            listItems.forEach((li, index) => {
                const bulletPrefix = ListProcessor.getUnorderedListPrefix(depth);
                ListProcessor.processListItem(li, bulletPrefix, fragment, index < listItems.length - 1, depth);
            });

            parentElement.insertBefore(fragment, ul);
            parentElement.removeChild(ul);
        }

        /**
         * 获取有序列表的前缀
         */
        static getOrderedListPrefix(number, depth) {
            const level = depth % 6;

            switch (level) {
                case 0: return `${number}. `;
                case 1: return `${number}) `;
                case 2: return `${NumberConverter.numberToLetter(number, true)}. `;
                case 3: return `${NumberConverter.numberToLetter(number, false)}. `;
                case 4: return `${NumberConverter.numberToRoman(number)}. `;
                case 5: return `${number}. `;
                default: return `${number}. `;
            }
        }

        /**
         * 获取无序列表的前缀
         */
        static getUnorderedListPrefix(depth) {
            const level = depth % CONSTANTS.LIST_SYMBOLS.UNORDERED.length;
            return `${CONSTANTS.LIST_SYMBOLS.UNORDERED[level]} `;
        }

        /**
         * 处理单个列表项
         */
        static processListItem(li, bulletPrefix, fragment, addSpacer, depth = 0) {
            // 计算缩进(每个层级2个空格)
            // 使用HTML non-breaking spaces (nbsp) 作为缩进
            // 这些会在HTML中占位,但视觉上几乎不可见
            const indentSpaces = '\u200d' + ' '.repeat(depth * 2);

            // 处理列表项中的直接子元素
            const children = Array.from(li.children);
            let firstTextProcessed = false;

            children.forEach((child, childIndex) => {
                const clonedChild = child.cloneNode(true);

                if (child.tagName === 'P') {
                    // 处理段落 - 只为段落添加缩进
                    if (!firstTextProcessed) {
                        // 第一个段落添加项目符号和缩进
                        clonedChild.innerHTML = `${indentSpaces}<strong>${bulletPrefix}</strong>${clonedChild.innerHTML}`;
                        firstTextProcessed = true;
                    } else {
                        // 后续段落只添加缩进
                        clonedChild.innerHTML = `${indentSpaces}${clonedChild.innerHTML}`;
                    }
                    fragment.appendChild(clonedChild);
                } else if (child.tagName === 'OL' || child.tagName === 'UL') {
                    // 嵌套列表已经在之前处理过了,这里跳过
                    return;
                } else {
                    // 其他元素直接添加,不做缩进处理
                    if (!firstTextProcessed && child.textContent.trim()) {
                        // 如果没有段落但有文本内容,创建一个段落
                        const contentP = document.createElement('p');
                        contentP.innerHTML = `${indentSpaces}<strong>${bulletPrefix}</strong>${clonedChild.innerHTML}`;
                        fragment.appendChild(contentP);
                        firstTextProcessed = true;
                    } else {
                        fragment.appendChild(clonedChild);
                    }
                }
            });

            // 如果没有处理任何文本内容,但列表项有内容,创建一个基本段落
            if (!firstTextProcessed && li.textContent.trim()) {
                const contentP = document.createElement('p');
                contentP.innerHTML = `${indentSpaces}<strong>${bulletPrefix}</strong>${li.innerHTML.replace(/<(ol|ul)[^>]*>[\s\S]*?<\/(ol|ul)>/gi, '')}`;
                fragment.appendChild(contentP);
            }
        }
    }

    // 内容处理类
    class ContentProcessor {
        /**
         * 处理引述块
         */
        static processBlockquote() {
            document.querySelectorAll("blockquote").forEach((item) => {
                const section = document.createElement("section");

                // 复制属性
                Array.from(item.attributes).forEach(attr => {
                    section.setAttribute(attr.name, attr.value);
                });

                // 复制样式
                const computedStyle = window.getComputedStyle(item);
                const styleProps = [
                    'margin', 'padding', 'border', 'border-left',
                    'background', 'background-color', 'color',
                    'font-size', 'font-weight', 'border-radius', 'line-height'
                ];

                styleProps.forEach(prop => {
                    const value = computedStyle.getPropertyValue(prop);
                    if (value) {
                        section.style.setProperty(prop, value);
                    }
                });

                section.innerHTML = item.innerHTML;
                item.parentNode.replaceChild(section, item);
            });
        }

        /**
         * 添加微信公众号名片
         */
        static addWeChatCard() {
            const typographyAreas = document.querySelectorAll('.b3-typography');

            typographyAreas.forEach(area => {
                if (area.querySelector('.mp_profile_iframe_wrp')) return;

                const elements = ContentProcessor.createWeChatCardElements();
                elements.forEach(element => area.appendChild(element));
            });
        }

        /**
         * 创建微信名片元素
         */
        static createWeChatCardElements() {
            const separatorP = document.createElement('p');
            separatorP.style.textAlign = 'center';
            separatorP.innerHTML = `<img src="${CONSTANTS.SEPARATOR_IMAGE_URL}" style="margin: 0px 1px;">`;

            const introSection = document.createElement('section');
            introSection.innerHTML = `<span leaf="">${CONSTANTS.WECHAT_PROFILE.introduction}</span>`;

            const cardSection = document.createElement('section');
            const profile = CONSTANTS.WECHAT_PROFILE;
            cardSection.innerHTML = `
                <section class="mp_profile_iframe_wrp custom_select_card_wrp" nodeleaf="">
                    <mp-common-profile class="mpprofile js_uneditable custom_select_card mp_profile_iframe" 
                        data-pluginname="mpprofile" 
                        data-nickname="${profile.nickname}" 
                        data-alias="${profile.alias}" 
                        data-headimg="${profile.headimg}" 
                        data-signature="${profile.signature}" 
                        data-id="${profile.id}" 
                        data-service_type="1" 
                        data-verify_status="1">
                    </mp-common-profile>
                    <br class="ProseMirror-trailingBreak">
                </section>`;

            return [separatorP, introSection, cardSection];
        }

        /**
         * 替换hr标签为图片
         */
        static replaceHrWithImage() {
            const typographyAreas = document.querySelectorAll('.b3-typography');

            typographyAreas.forEach(area => {
                const hrElements = area.querySelectorAll('hr');
                hrElements.forEach(hr => {
                    const pElement = document.createElement('p');
                    pElement.style.textAlign = 'center';

                    const imgElement = document.createElement('img');
                    imgElement.src = CONSTANTS.SEPARATOR_IMAGE_URL;
                    imgElement.style.margin = '0 1px';

                    pElement.appendChild(imgElement);
                    hr.parentNode.replaceChild(pElement, hr);
                });
            });
        }

        /**
         * 处理数学公式末尾空格问题
         */
        static processMathFormulas() {
            const typographyDivs = document.querySelectorAll('div.b3-typography');

            typographyDivs.forEach(typographyDiv => {
                const lastChild = typographyDiv.lastElementChild;
                if (lastChild?.matches('div[data-subtype="math"]')) {
                    const newParagraph = document.createElement('p');
                    newParagraph.innerHTML = '';
                    typographyDiv.appendChild(newParagraph);
                }
            });
        }

        /**
         * 将span[data-type="code"]转换为code元素(适用于知乎)
         */
        static convertSpanCodeToCodeElement() {
            const typographyAreas = document.querySelectorAll('.b3-typography');

            typographyAreas.forEach(area => {
                const codeSpans = area.querySelectorAll('span[data-type="code"]');

                codeSpans.forEach(span => {
                    const codeElement = document.createElement('code');

                    // 复制文本内容
                    codeElement.textContent = span.textContent;

                    // 复制样式(如果需要)
                    const computedStyle = window.getComputedStyle(span);
                    const styleProps = [
                        'background-color', 'color', 'font-family',
                        'font-size', 'padding', 'border-radius'
                    ];

                    styleProps.forEach(prop => {
                        const value = computedStyle.getPropertyValue(prop);
                        if (value && value !== 'none' && value !== 'auto') {
                            codeElement.style.setProperty(prop, value);
                        }
                    });

                    // 替换原始span元素
                    span.parentNode.replaceChild(codeElement, span);
                });
            });
        }

        /**
         * 将思源笔记图片格式转换为知乎格式(带标题支持)
         */
        static convertSiyuanImagesToZhihuFormat() {
            const typographyAreas = document.querySelectorAll('.b3-typography');

            typographyAreas.forEach(area => {
                const imageSpans = area.querySelectorAll('span[data-type="img"]');

                imageSpans.forEach(span => {
                    const imgElement = span.querySelector('img');
                    if (!imgElement) return;

                    // 获取图片信息
                    const src = imgElement.getAttribute('src') || imgElement.getAttribute('data-src');
                    const alt = imgElement.getAttribute('alt') || '';
                    const title = imgElement.getAttribute('title') || '';

                    // 获取标题文本(从 protyle-action__title 中获取)
                    const titleSpan = span.querySelector('.protyle-action__title span');
                    const captionText = titleSpan ? titleSpan.textContent.trim() : (title || alt);

                    // 创建知乎格式的figure元素
                    const figure = document.createElement('figure');

                    // 创建img元素
                    const newImg = document.createElement('img');
                    newImg.setAttribute('src', src);

                    figure.appendChild(newImg);

                    // 如果有标题,添加figcaption
                    if (captionText) {
                        const figcaption = document.createElement('figcaption');
                        figcaption.textContent = captionText;
                        figure.appendChild(figcaption);
                    }

                    // 检查父级p标签是否只包含图片和零宽空格
                    const parentP = span.closest('p');
                    if (parentP) {
                        // 获取p标签的文本内容并清理零宽空格
                        const pTextContent = parentP.textContent || '';
                        const cleanedText = pTextContent.replace(/[\u200B\u200C\u200D\uFEFF]/g, '').trim();
                      
                        // 如果p标签只包含图片(清理后无其他文本内容)
                        if (cleanedText === '' || cleanedText === alt || cleanedText === title) {
                            // 直接替换整个p标签
                            parentP.parentNode.replaceChild(figure, parentP);
                        } else {
                            // 如果p标签还有其他内容,只替换span元素
                            span.parentNode.replaceChild(figure, span);
                        }
                    } else {
                        // 替换原始的span元素
                        span.parentNode.replaceChild(figure, span);
                    }
                });

                // 清理包含图片的p标签中的零宽空格
                const paragraphs = area.querySelectorAll('p');
                paragraphs.forEach(p => {
                    if (p.querySelector('img') || p.querySelector('figure')) {
                        // 清理文本节点中的零宽空格
                        const walker = document.createTreeWalker(
                            p,
                            NodeFilter.SHOW_TEXT,
                            null,
                            false
                        );

                        const textNodes = [];
                        let node;
                        while (node = walker.nextNode()) {
                            textNodes.push(node);
                        }

                        textNodes.forEach(textNode => {
                            const cleanedText = textNode.textContent.replace(/[\u200B\u200C\u200D\uFEFF]/g, '');
                            if (cleanedText !== textNode.textContent) {
                                textNode.textContent = cleanedText;
                            }
                        });

                        // 如果p标签清理后只剩空白内容和图片/figure,移除p标签保留内容
                        const remainingText = p.textContent.replace(/[\u200B\u200C\u200D\uFEFF\s]/g, '');
                        if (remainingText === '' && (p.querySelector('img') || p.querySelector('figure'))) {
                            const children = Array.from(p.children);
                            const parent = p.parentNode;
                            children.forEach(child => {
                                parent.insertBefore(child, p);
                            });
                            parent.removeChild(p);
                        }
                    }
                });
            });
        }

        /**
         * 将思源笔记代码块转换为知乎格式(带语言高亮)
         */
        static convertCodeBlocksForZhihu() {
            const typographyAreas = document.querySelectorAll('.b3-typography');

            typographyAreas.forEach(area => {
                const codeBlocks = area.querySelectorAll('pre.code-block[data-language]');

                codeBlocks.forEach(preElement => {
                    const language = preElement.getAttribute('data-language') || '';
                    const codeElement = preElement.querySelector('code');

                    if (!codeElement) return;

                    // 提取纯文本内容(去除HTML标签)
                    const codeText = ContentProcessor.extractPlainTextFromCode(codeElement);

                    // 创建新的知乎格式代码块
                    const newPre = document.createElement('pre');
                    if (language) {
                        newPre.setAttribute('lang', language);
                    }
                    newPre.textContent = codeText;

                    // 替换原始代码块
                    preElement.parentNode.replaceChild(newPre, preElement);
                });
            });
        }

        /**
         * 从代码元素中提取纯文本内容
         */
        static extractPlainTextFromCode(codeElement) {
            // 创建一个临时div来处理HTML内容
            const tempDiv = document.createElement('div');
            tempDiv.innerHTML = codeElement.innerHTML;

            // 递归提取文本内容,保持换行符
            const extractText = (node) => {
                if (node.nodeType === Node.TEXT_NODE) {
                    return node.textContent;
                } else if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node.tagName === 'BR') {
                        return '\n';
                    }
                    let text = '';
                    for (const child of node.childNodes) {
                        text += extractText(child);
                    }
                    return text;
                }
                return '';
            };

            return extractText(tempDiv).trim();
        }

    }

    // 标题编号处理类
    class HeadingNumberProcessor {
        /**
         * 为HTML中的标题添加编号
         */
        static addNumbersToHTMLHeadings() {
            const typographyAreas = document.querySelectorAll('.b3-typography');

            typographyAreas.forEach(area => {
                HeadingNumberProcessor.processHeadingsInArea(area);
            });
        }

        /**
         * 处理指定区域内的标题
         */
        static processHeadingsInArea(area) {
            const headings = area.querySelectorAll('h1, h2, h3, h4, h5, h6');
            const numbers = Array(6).fill(0); // 用于存储各级标题的编号

            headings.forEach(heading => {
                const level = parseInt(heading.tagName.substring(1)); // 获取标题级别

                if (level >= CONSTANTS.HEADING_NUMBER.START_LEVEL &&
                    level <= CONSTANTS.HEADING_NUMBER.END_LEVEL) {

                    // 更新当前级别的编号
                    numbers[level - 1]++;

                    // 重置更深层级的编号
                    for (let i = level; i < 6; i++) {
                        numbers[i] = 0;
                    }

                    // 生成编号字符串
                    const numberStr = HeadingNumberProcessor.generateNumberString(numbers, level);

                    // 检查是否已经有编号
                    if (!HeadingNumberProcessor.hasExistingNumber(heading)) {
                        HeadingNumberProcessor.addNumberToHeading(heading, numberStr);
                    }
                }
            });
        }

        /**
         * 生成编号字符串
         */
        static generateNumberString(numbers, level) {
            const relevantNumbers = numbers.slice(0, level).filter(num => num > 0);
            return relevantNumbers.join(CONSTANTS.HEADING_NUMBER.SEPARATOR) + ' ';
        }

        /**
         * 检查标题是否已有编号
         */
        static hasExistingNumber(heading) {
            const text = heading.textContent.trim();
            return /^\d+(\.\d+)*\s/.test(text);
        }

        /**
         * 为标题添加编号
         */
        static addNumberToHeading(heading, numberStr) {
            const existingContent = heading.innerHTML;
            heading.innerHTML = `<strong>${numberStr}</strong>${existingContent}`;
        }

        /**
         * 为markdown内容中的标题添加编号
         */
        static addNumbersToMarkdownHeadings(content) {
            const lines = content.split('\n');
            const numbers = Array(6).fill(0);
            let inCodeBlock = false;
            let codeBlockFence = '';

            const processedLines = lines.map(line => {
                // 检测代码块边界
                const codeBlockMatch = line.match(/^(\s*)(‍```|~~~)(.*)$/);
                if (codeBlockMatch) {
                    const [, indent, fence, language] = codeBlockMatch;
                    if (!inCodeBlock) {
                        // 开始代码块
                        inCodeBlock = true;
                        codeBlockFence = fence;
                    } else if (fence === codeBlockFence && indent.length === 0) {
                        // 结束代码块(需要相同的围栏符号且在行首)
                        inCodeBlock = false;
                        codeBlockFence = '';
                    }
                    return line;
                }

                // 如果在代码块内,直接返回原行
                if (inCodeBlock) {
                    return line;
                }

                // 检测行内代码块(单行)
                const inlineCodeCount = (line.match(/`/g) || []).length;
                const hasInlineCode = inlineCodeCount >= 2 && inlineCodeCount % 2 === 0;

                // 处理标题(只在非代码块内)
                const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);

                if (headingMatch && !hasInlineCode) {
                    const level = headingMatch[1].length;
                    const headingText = headingMatch[2];

                    if (level >= CONSTANTS.HEADING_NUMBER.START_LEVEL &&
                        level <= CONSTANTS.HEADING_NUMBER.END_LEVEL) {

                        // 更新当前级别的编号
                        numbers[level - 1]++;

                        // 重置更深层级的编号
                        for (let i = level; i < 6; i++) {
                            numbers[i] = 0;
                        }

                        // 检查是否已有编号
                        if (!/^\d+(\.\d+)*\s/.test(headingText)) {
                            const numberStr = HeadingNumberProcessor.generateNumberString(numbers, level);
                            return `${'#'.repeat(level)} ${numberStr}${headingText}`;
                        }
                    }
                }

                return line;
            });

            return processedLines.join('\n');
        }
    }

    // 按钮处理类
    class ButtonHandler {
        /**
         * 微信公众号按钮点击处理
         */
        static async handleWechatButtonClick(event) {
            const button = event.currentTarget;
            const wrapper = button.closest('.mp-wechat-enhanced-controls');
            const select = wrapper?.querySelector('.link-conversion-select');
            const headingNumberSelect = wrapper?.querySelector('.heading-number-select');
            const imageHostSelect = wrapper?.querySelector('.image-host-select');
            const linkConversionOption = select ? select.value : 'convert';
            const addHeadingNumbers = headingNumberSelect ? headingNumberSelect.value === 'yes' : false;
            const imageHostType = imageHostSelect ? imageHostSelect.value : CONSTANTS.IMAGE_HOST_TYPE.DEFAULT;

            await Utils.showNotification("发布到微信公众号:样式转换ing");

            try {
                // 根据图床类型处理图片URL
                if (imageHostType === CONSTANTS.IMAGE_HOST_TYPE.PICGO) {
                    const docId = Utils.getCurrentDocumentId();
                    if (docId) {
                        const picgoFileMapKey = await Utils.getDocumentAttribute(docId, 'custom-picgo-file-map-key');
                        if (picgoFileMapKey) {
                            const picgoFileMap = JSON.parse(picgoFileMapKey);
                            await ImageProcessor.replaceImageUrlsInDOM(picgoFileMap);
                        }
                    }
                }
                // 使用默认图床时,微信不需要特殊处理

                // 添加标题编号
                if (addHeadingNumbers) {
                    HeadingNumberProcessor.addNumbersToHTMLHeadings();
                }

                // 处理链接转换
                if (linkConversionOption === 'convert') {
                    await LinkProcessor.convertLinksToWechat();
                } else if (linkConversionOption === 'no-convert') {
                    await LinkProcessor.convertBlockReferencesToWechat();
                }

                // 处理内容
                ContentProcessor.replaceHrWithImage();
                ContentProcessor.addWeChatCard();
                ContentProcessor.processBlockquote();

                // 点击桌面按钮
                const desktopButton = document.querySelector('.layout__wnd--active .protyle:not(.fn__none) .protyle-preview .protyle-preview__action > button[data-type="desktop"]');
                desktopButton?.click();

                await Utils.showNotification("发布到微信公众号:样式转换完成");

                // 点击微信复制按钮
                const wechatCopyButton = document.querySelector('.layout__wnd--active .protyle:not(.fn__none) .protyle-preview .protyle-preview__action > button[data-type="mp-wechat"]');
                wechatCopyButton?.click();

            } catch (error) {
                console.error('微信处理过程中出错:', error);
                await Utils.showNotification(`处理失败: ${error.message}`);
            }
        }

        /**
         * 新知乎按钮点击处理
         */
        static async handleNewZhihuButtonClick(event) {
            await Utils.showNotification("发布到知乎:样式转换ing");

            try {
                // 获取图床选择
                const imageHostSelect = document.querySelector('.image-host-select');
                const imageHostType = imageHostSelect ? imageHostSelect.value : CONSTANTS.IMAGE_HOST_TYPE.DEFAULT;

                // 根据图床类型处理图片URL
                if (imageHostType === CONSTANTS.IMAGE_HOST_TYPE.PICGO) {
                    const docId = Utils.getCurrentDocumentId();
                    if (docId) {
                        const picgoFileMapKey = await Utils.getDocumentAttribute(docId, 'custom-picgo-file-map-key');
                        if (picgoFileMapKey) {
                            const picgoFileMap = JSON.parse(picgoFileMapKey);
                            await ImageProcessor.replaceImageUrlsInDOM(picgoFileMap);
                        }
                    }
                }
                // 使用默认图床时,知乎不需要特殊处理

                // 添加标题编号
                const headingNumberSelect = document.querySelector('.heading-number-select');
                const addHeadingNumbers = headingNumberSelect ? headingNumberSelect.value === 'yes' : false;
                if (addHeadingNumbers) {
                    HeadingNumberProcessor.addNumbersToHTMLHeadings();
                }

                // 转换思源笔记图片格式为知乎格式
                ContentProcessor.convertSiyuanImagesToZhihuFormat();

                // 转换代码块为知乎格式
                ContentProcessor.convertCodeBlocksForZhihu();

                // 预处理有序列表
                ListProcessor.processOrderedListsForZhihu();

                // 转换行内代码元素
                ContentProcessor.convertSpanCodeToCodeElement();

                await LinkProcessor.convertBlockReferencesToWechat();

                await Utils.showNotification("发布到知乎:样式转换完成");

                // 点击原始知乎按钮
                const originalZhihuButton = document.querySelector('.layout__wnd--active .protyle:not(.fn__none) .protyle-preview .protyle-preview__action > button[data-type="zhihu"]');
                originalZhihuButton?.click();

            } catch (error) {
                console.error('知乎处理过程中出错:', error);
                await Utils.showNotification(`处理失败: ${error.message}`);
            }
        }

        /**
         * Markdown按钮点击处理
         */
        static async handleMarkdownButtonClick(event) {
            await Utils.showNotification("正在处理文档导出到Markdown...", 3000);

            try {
                const docId = Utils.getCurrentDocumentId();
                if (!docId) {
                    throw new Error('无法获取当前文档ID');
                }

                // 获取图床选择
                const imageHostSelect = document.querySelector('.image-host-select');
                const imageHostType = imageHostSelect ? imageHostSelect.value : CONSTANTS.IMAGE_HOST_TYPE.DEFAULT;

                // 导出markdown内容
                const markdownContent = await ButtonHandler.exportMarkdownContent(docId);
                let processedContent = markdownContent;

                // 获取标题编号选项
                const headingNumberSelect = document.querySelector('.heading-number-select');
                const addHeadingNumbers = headingNumberSelect ? headingNumberSelect.value === 'yes' : false;

                // 添加标题编号
                if (addHeadingNumbers) {
                    processedContent = HeadingNumberProcessor.addNumbersToMarkdownHeadings(processedContent);
                }

                // 根据图床类型处理图片URL
                if (imageHostType === CONSTANTS.IMAGE_HOST_TYPE.PICGO) {
                    // 如果选择PicGo图床,使用图床映射替换
                    const picgoFileMapKey = await Utils.getDocumentAttribute(docId, 'custom-picgo-file-map-key');
                    if (picgoFileMapKey) {
                        processedContent = await ImageProcessor.replaceImageUrls(processedContent, picgoFileMapKey);
                    } else {
                        // 没有图床映射时为相对路径图片添加默认前缀
                        processedContent = ImageProcessor.addDefaultPrefixToImages(processedContent);
                    }
                } else {
                    // 如果选择默认图床,为相对路径图片添加默认前缀
                    processedContent = ImageProcessor.addDefaultPrefixToImages(processedContent);
                }

                // 处理块链接
                processedContent = await LinkProcessor.convertBlockLinksToWeChat(processedContent);

                // 复制到剪贴板
                await Utils.copyToClipboard(processedContent);
                await Utils.showNotification("文档已处理完成并复制到剪贴板!", 3000);

            } catch (error) {
                console.error('处理文档时出错:', error);
                await Utils.showNotification(`处理失败: ${error.message}`, 5000);
            }
        }

        /**
         * 导出markdown内容
         */
        static async exportMarkdownContent(docId) {
            const data = { id: docId, yfm: false };
            const res = await Utils.fetchSyncPost('/api/export/exportMdContent', data);

            if (!res?.data?.content) {
                throw new Error('导出markdown内容失败');
            }
            return res.data.content;
        }
    }

    // UI管理类
    class UIManager {
        /**
         * 添加自定义按钮
         */
        static addCustomButton() {
            const actionContainers = document.querySelectorAll('.protyle-preview .protyle-preview__action');

            actionContainers.forEach(container => {
                // 检查是否已添加按钮以避免重复
                if (container.querySelector('.mp-wechat-enhanced-controls') ||
                    container.querySelector('.markdown-enhanced-button')) {
                    return;
                }

                UIManager.addWeChatEnhancedControls(container);
                UIManager.addNewZhihuButton(container);
                UIManager.addMarkdownButton(container);
            });

            ContentProcessor.processMathFormulas();
        }

        /**
         * 添加微信公众号增强控件
         */
        static addWeChatEnhancedControls(container) {
            const controlsWrapper = document.createElement('div');
            controlsWrapper.className = 'mp-wechat-enhanced-controls';
            Object.assign(controlsWrapper.style, {
                display: 'inline-flex',
                alignItems: 'center',
                marginLeft: '8px',
                gap: '4px'
            });

            // 创建微信按钮
            const wechatButton = UIManager.createButton({
                type: 'mp-wechat-enchaced',
                label: '粘贴到公众号样式适配',
                icon: '#iconMp',
                handler: ButtonHandler.handleWechatButtonClick
            });

            // 创建链接转换选择下拉框
            const linkSelect = UIManager.createLinkConversionSelect();

            // 创建标题编号选择下拉框
            const headingSelect = UIManager.createHeadingNumberSelect();

            // 创建图床选择下拉框
            const imageHostSelect = UIManager.createImageHostSelect();

            controlsWrapper.appendChild(wechatButton);
            controlsWrapper.appendChild(linkSelect);
            controlsWrapper.appendChild(headingSelect);
            controlsWrapper.appendChild(imageHostSelect);

            // 插入到容器中
            const originalWechatButton = container.querySelector('button[data-type="mp-wechat"]');
            if (originalWechatButton) {
                container.insertBefore(controlsWrapper, originalWechatButton);
            } else {
                container.appendChild(controlsWrapper);
            }
        }

        /**
         * 添加新知乎按钮
         */
        static addNewZhihuButton(container) {
            const zhihuButton = container.querySelector('button[data-type="zhihu"]');
            if (!zhihuButton || container.querySelector('button[data-type="new_zhihu"]')) {
                return;
            }

            const newZhihuButton = UIManager.createButton({
                type: 'new_zhihu',
                label: '知乎样式转换(仅处理思源块链接)',
                icon: '#iconZhihu',
                handler: ButtonHandler.handleNewZhihuButtonClick
            });

            zhihuButton.parentNode.insertBefore(newZhihuButton, zhihuButton.nextSibling);
        }

        /**
         * 添加Markdown按钮
         */
        static addMarkdownButton(container) {
            const markdownButton = UIManager.createButton({
                type: 'markdown-enhanced',
                className: 'markdown-enhanced-button',
                label: '复制Markdown(图床链接转换)',
                icon: '#iconMarkdown',
                handler: ButtonHandler.handleMarkdownButtonClick
            });

            container.appendChild(markdownButton);
        }

        /**
         * 创建按钮
         */
        static createButton({ type, className = '', label, icon, handler }) {
            const button = document.createElement('button');
            button.type = 'button';
            button.dataset.type = type;
            button.dataset.custom = 'true';
            button.className = `b3-tooltips b3-tooltips__w ${className}`;
            button.setAttribute('aria-label', label);
            button.innerHTML = `<svg><use xlink:href="${icon}"></use></svg>`;

            Object.assign(button.style, {
                color: "var(--b3-theme-primary)",
                backgroundColor: "#e6f7ff",
                border: "1px solid #91d5ff",
                borderRadius: "4px",
                padding: "4px 8px",
                marginLeft: "8px"
            });

            button.addEventListener('click', handler);
            return button;
        }

        /**
         * 创建链接转换选择下拉框
         */
        static createLinkConversionSelect() {
            const select = document.createElement('select');
            select.className = 'link-conversion-select b3-select';
            Object.assign(select.style, {
                height: '24px',
                fontSize: '12px',
                minWidth: '120px'
            });

            const options = [
                { value: 'convert', text: '微信公众号外链暴露', selected: true },
                { value: 'no-convert', text: '仅处理思源块链接' }
            ];

            options.forEach(({ value, text, selected }) => {
                const option = document.createElement('option');
                option.value = value;
                option.textContent = text;
                option.selected = selected || false;
                select.appendChild(option);
            });

            return select;
        }

        /**
         * 创建标题编号选择下拉框
         */
        static createHeadingNumberSelect() {
            const select = document.createElement('select');
            select.className = 'heading-number-select b3-select';
            Object.assign(select.style, {
                height: '24px',
                fontSize: '12px',
                minWidth: '100px'
            });

            const options = [
                { value: 'no', text: '不添加编号', selected: false },
                { value: 'yes', text: '添加标题编号', selected: true }
            ];

            options.forEach(({ value, text, selected }) => {
                const option = document.createElement('option');
                option.value = value;
                option.textContent = text;
                option.selected = selected || false;
                select.appendChild(option);
            });

            return select;
        }

        /**
         * 创建图床选择下拉框
         */
        static createImageHostSelect() {
            const select = document.createElement('select');
            select.className = 'image-host-select b3-select';
            Object.assign(select.style, {
                height: '24px',
                fontSize: '12px',
                minWidth: '100px'
            });

            const options = [
                { value: CONSTANTS.IMAGE_HOST_TYPE.DEFAULT, text: '默认图床', selected: true },
                { value: CONSTANTS.IMAGE_HOST_TYPE.PICGO, text: 'PicGo图床', selected: false }
            ];

            options.forEach(({ value, text, selected }) => {
                const option = document.createElement('option');
                option.value = value;
                option.textContent = text;
                option.selected = selected || false;
                select.appendChild(option);
            });

            return select;
        }

        /**
         * 创建选择下拉框(保持兼容性)
         */
        static createSelect() {
            return UIManager.createLinkConversionSelect();
        }

        // ...existing code...
    }

    // DOM观察器
    class DOMObserver {
        /**
         * 观察DOM变化
         */
        static observeDomChange(targetNode, callback) {
            const config = { childList: true, subtree: true };
            const observer = new MutationObserver((mutationsList) => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList') {
                        callback(mutation);
                    }
                }
            });
            observer.observe(targetNode, config);
            return observer;
        }
    }

    // 初始化
    function init() {
        const targetNode = document.body;
        DOMObserver.observeDomChange(targetNode, (mutation) => {
            if (mutation.target.querySelector('.b3-typography')) {
                UIManager.addCustomButton();
            }
        });
    }

    // 启动应用
    init();
})();

2 如何使用代码片段

PixPin_2025-07-09_12-00-04

PixPin_2025-07-09_12-01-25

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    26296 引用 • 109308 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    201 引用 • 1443 回帖 • 2 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • Achuan-2

    更新了对知乎的特殊处理,知乎多级列表的支持很差,列表不能放图片和代码块,每次思源笔记的多级列表粘贴到知乎效果就会很差,于是终于下定决心改了下,这下以后发知乎就舒服多了

  • Achuan-2

    已支持复制到微信公众号、知乎、Gihub 的标题编号,不喜欢在思源本体进行标题编号,因为不方便直接把标题内容复制到其他文章,到时候还要删编号

    PixPin20250629190511.png

  • Achuan-2
    - 20250709 v3.0
      - 粘贴到知乎,支持图片标题
      - 改进复制到知乎的代码块高亮,粘贴支持直接高亮