[js] 思源笔记粘贴到微信公众号处理公式、链接、多级列表

思源笔记发布到微信公众号,对于公式、链接、多级列表会有一些问题,终于忍无可忍,让 GPT 给我写了处理的代码,现在这三种样式都能很好地粘贴到微信公众号啦。再加上我开发的 Tsundoku 主题对发布到微信公众号做了一点样式处理,现在基本上,思源笔记是什么样式,发到微信公众号也是什么样式了,舒服!以后就可以专心写公众号啦!不用每次发布,都折腾排版问题、样式缺失问题。

数学公式处理

处理方法

思源笔记的公式不能直接复制粘贴到微信公众号,于是将公式替换为 svg 来粘贴

js 代码

function loadMathJax(callback) {
    window.MathJax = {
        tex: {
            inlineMath: [
                ['$', '$'],
                ['\\(', '\\)']
            ]
        },
        svg: {
            fontCache: 'none'
        }
    };

    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';
    script.async = true;
    script.onload = callback;
    document.head.appendChild(script);
}

function createRenderer(display) {
    return (mathContent) => {
        const cleanedContent = mathContent.replace(/\\displaystyle\s*{([^}]*)}/g, '$1');

        window.MathJax.texReset();
        const mjxContainer = window.MathJax.tex2svg(cleanedContent, { display });
        const svg = mjxContainer.firstChild;
        const width = svg.style[`min-width`] || svg.getAttribute(`width`);
        svg.removeAttribute(`width`);
        svg.style = `max-width: 70vw !important;`;
        svg.style.width = width;
        svg.style.display = `initial`;
        if (display) {
            return `<section style="box-sizing: border-box; border-width: 0px; border-style: solid; border-color: hsl(var(--border)); user-select: text !important; color: rgb(10, 10, 10); font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; text-align: center; overflow: auto;">${svg.outerHTML}</section>`;
        }
        return svg.outerHTML;
    };
}
async function renderMathBlocks() {
    const mathBlocks = document.querySelectorAll('.b3-typography div[data-subtype="math"]:not([data-repleaced="true"])');
    const renderBlock = createRenderer(true);

    return Promise.all(Array.from(mathBlocks).map(async (block) => {
        const mathContent = block.getAttribute('data-content');
        try {
            block.innerHTML = renderBlock(mathContent);
            block.setAttribute('data-repleaced', 'true');
        } catch (error) {
            console.error('Error rendering MathJax block:', error);
        }
    }));
}

async function renderInlineMath() {
    const inlineMaths = document.querySelectorAll('span[data-type="inline-math"]:not([data-repleaced="true"])');
    const renderInline = createRenderer(false);

    return Promise.all(Array.from(inlineMaths).map(async (span) => {
        const mathContent = span.getAttribute('data-content');
        try {
            span.innerHTML = renderInline(mathContent);
            span.style.verticalAlign = 'middle';
            span.style.lineHeight = '1';
            span.setAttribute('data-repleaced', 'true');
        } catch (error) {
            console.error('Error rendering MathJax inline:', error);
        }
    }));
}


loadMathJax(async () => {
    await Promise.all([renderMathBlocks(), renderInlineMath()]);
    console.log('MathJax has been loaded!');
});

处理后的结果

公式举例

行内公式:f(x) = \frac{\sqrt{x^2 + 1} + \log_e(x + 2) - \sin(\pi x)}{e^{x^2} + \int_0^x t^2 \, dt}

行间公式:麦克斯韦方程组

\begin{array}{lll} \nabla\times E & = & -\;\frac{\partial{B}}{\partial{t}} \ \nabla\times H & = & \frac{\partial{D}}{\partial{t}}+J \ \nabla\cdot D & = & \rho \ \nabla\cdot B & = & 0 \ \end{array}

修改前,发到公众号的样式

PixPin_2024-11-14_10-30-07

修改后,发到公众号的样式

PixPin_2024-11-14_10-46-57

处理链接

处理方法

写 js 代码,处理思源笔记预览模式的超链接,以发送到微信公众号

  1. 微信公众号的外链(除了 https://mp.weixin.qq.com/.*以外的链接)是不能直接跳转到,所以要把思源笔记的超链接都变为普通文本,然后链接放在锚文本的后面,即变为 [锚文本](链接) 的形式
  2. 如果外链锚文本本身就是链接,就不需要用 [text]({href}) 样式
  3. 对转换后的链接添加 color=#338dd6

js 代码

function convertLinksToWechat() {
    // Select all links
    const links = document.querySelectorAll('.b3-typography a');

    links.forEach(link => {
        // Get the href and text content from the link
        const href = link.getAttribute('href');
        const text = link.textContent;

        // Check if the link is not a WeChat internal link
        if (!href.startsWith('https://mp.weixin.qq.com/')) {
            let newTextContent;

            // Check if the text is the same as the href
            if (text === href) {
                // If the text is the same as the href, just use the href
                newTextContent = href;
            } else {
                // Otherwise, format as [text](href)
                newTextContent = `[${text}](${href})`;
            }

            // Create a new span element to hold the text with red color
            const newSpan = document.createElement('span');
            newSpan.style.color = '#338dd6';
            newSpan.textContent = newTextContent;

            // Replace the link with the new span element
            link.parentNode.replaceChild(newSpan, link);
        }
    });
}

处理后的结果

链接样式举例

修改前,发到公众号的样式(注意外链在微信公众号编辑器是有颜色的,但是一旦预览和发布,就会变为普通文本)

PixPin_2024-11-14_10-30-28

修改后,发到公众号的样式

PixPin_2024-11-14_10-46-14

处理多级列表

处理方法

思源笔记目前的渲染是,次级的列表会包含在 <li> 中,而微信则是需要次级列表和 <li> 同级。
目前复制的多级列表 html 结构为:

<ul>
	<li>
		<ul>
			<li>
				<ul></ul>
			</li>
		</ul>
	</li>
</ul>

微信公众号需要正常渲染的预期如下:

<ul>
	<li></li>
	<ul>
		<li></li>
		<ul>
               </ul>
	</ul>
</ul>

请写一个 js 代码对思源笔记的列表 DOM 结构进行重新调整,包括

      function convertListToWechat() {
          // Select all <ul> and <ol> elements
          const lists = document.querySelectorAll('ul, ol');
      
          lists.forEach(list => {
              // Get all <li> children of the current list
              const items = Array.from(list.children);
      
              items.forEach(item => {
                  // Check if the <li> has a nested <ul> or <ol> as its direct child
                  const nestedList = item.querySelector('ul, ol');
                  if (nestedList) {
                      // Move the nested list to be a sibling of the current <li>
                      item.parentNode.insertBefore(nestedList, item.nextSibling);
                  }
              });
          });
      }
      
      // Run the function to adjust the list structure
      convertListToWechat();
      

      处理后的结果

      多级列表举例

      • 多巴胺

        1. 基本概念

          • 多巴胺是一种神经递质,属于儿茶酚胺和苯乙胺家族。
          • 它在大脑和身体中扮演着多种角色,尤其是在奖励、动机和愉悦感的调节中。
          • 化学式为C_8H_{11}NO_2,其结构中包含一个苯环和一个胺基。
        2. 生理功能

          • 奖励与动机

            • 多巴胺在大脑的奖励系统中起关键作用,影响动机和愉悦感。
            • 它的释放与愉快的体验和行为强化有关。
          • 运动控制

            • 在中脑的黑质区,多巴胺调节运动功能。
            • 多巴胺缺乏与帕金森病有关,导致运动障碍。
          • 情绪与认知

            • 多巴胺影响情绪调节和认知功能。
            • 其失衡可能与精神疾病如抑郁症和精神分裂症有关。
        3. 合成与代谢

          • 合成

            • 多巴胺由氨基酸酪氨酸通过一系列酶促反应合成。
            • 关键步骤包括酪氨酸羟化酶催化生成左旋多巴(L-DOPA),然后由多巴脱羧酶转化为多巴胺。
          • 代谢

            • 多巴胺通过单胺氧化酶(MAO)和儿茶酚-O-甲基转移酶(COMT)代谢。
            • 代谢产物包括高香草酸(HVA),可通过尿液排出。
        4. 临床相关性

          • 帕金森病

            • 由于黑质区多巴胺神经元的退化,导致运动功能障碍。
            • 治疗通常涉及多巴胺替代疗法,如左旋多巴。
          • 精神疾病

            • 精神分裂症与多巴胺系统的过度活跃有关。
            • 抗精神病药物通常通过阻断多巴胺受体来发挥作用。
          • 成瘾

            • 多巴胺在成瘾行为中起重要作用,许多药物通过增加多巴胺释放或阻止其再摄取来产生愉悦感。
            • 成瘾治疗可能涉及调节多巴胺系统的药物。

      修改前,发布到微信公众号的样式

      PixPin_2024-11-14_10-48-40

      修改后,发到公众号的样式

      PixPin_2024-11-14_10-45-27

      完整 js 代码片段

      预览模式添加一个按钮,点击之后自动处理公式、链接、多级列表,并复制内容,可以直接粘贴到微信公众号

      (() => {
          // Start observing DOM changes
          const targetNode = document.body;
          observeDomChange(targetNode, (mutation) => {
              if (mutation.target.querySelector('.b3-typography')) {
                  addCustomButton();
              }
          });
      
          // Function to add the custom button
          function addCustomButton() {
              const actionContainers = document.querySelectorAll('.protyle-preview .protyle-preview__action');
              actionContainers.forEach(container => {
                  // Check if the custom button already exists
                  if (!container.querySelector('button[data-type="mp-wechat-enchaced')) {
                      const button = document.createElement('button');
                      button.type = 'button';
                      button.dataset.type = 'mp-wechat-enchaced';
                      button.dataset.custom = 'true';
                      button.className = 'b3-tooltips b3-tooltips__w';
                      button.setAttribute('aria-label', '粘贴到公众号样式适配');
                      button.innerHTML = '<svg><use xlink:href="#iconMp"></use></svg>';
                      button.style.color = "var(--b3-theme-primary)";
                      button.onclick = handleButtonClick;
      
                      // Find the existing desktop button
                      const desktopButton = container.querySelector('button[data-type="mp-wechat"]');
                      if (desktopButton) {
                          // Insert the custom button before the desktop button
                          container.insertBefore(button, desktopButton);
                      } else {
                          // If desktop button doesn't exist, append the custom button at the end
                          container.appendChild(button);
                      }
                  }
              });
          }
      
          // Function to handle button click
          async function handleButtonClick() {
      
              await fetchSyncPost('/api/notification/pushMsg', {
                  "msg": "发布到微信公众号:样式转换ing",
                  "timeout": 5000
              });
              convertLinksToWechat();
              convertListToWechat();
              await convertMathToWechat();
      
              const desktopButton = document.querySelector('.layout__wnd--active .protyle:not(.fn__none) .protyle-preview .protyle-preview__action > button[data-type="desktop"]');
              if (desktopButton) {
                  desktopButton.click();
              }
            
              await fetchSyncPost('/api/notification/pushMsg', {
                  "msg": "发布到微信公众号:样式转换完成",
                  "timeout": 5000
              });
      
      
              // Simulate click on the existing button to copy content  
              const wechatCopyButton = document.querySelector('.layout__wnd--active .protyle:not(.fn__none) .protyle-preview .protyle-preview__action > button[data-type="mp-wechat"]');
              if (wechatCopyButton) {
                  wechatCopyButton.click();
              }
          }
      
          // Function to convert SiYuan note links to plain text with link
          function convertLinksToWechat() {
              const links = document.querySelectorAll('.b3-typography a');
              links.forEach(link => {
                  const href = link.getAttribute('href');
                  const text = link.textContent;
      
                  // Check if the link starts with 'siyuan://'
                  if (href.startsWith('siyuan://')) {
                      // Create a new span element with the link text
                      const newSpan = document.createElement('span');
                      newSpan.textContent = text;
                      newSpan.style.color = '#338dd6'; // Set the text color
      
                      // Replace the original link with the new span element
                      link.parentNode.replaceChild(newSpan, link);
                  } else if (!href.startsWith('https://mp.weixin.qq.com/')) {
                      const newTextContent = text === href ? href : `[${text}](${href})`;
                      const newSpan = document.createElement('span');
                      newSpan.style.color = '#338dd6';
                      newSpan.textContent = newTextContent;
                      link.parentNode.replaceChild(newSpan, link);
                  }
              });
          }
      
          // Function to adjust list structure for WeChat
          function convertListToWechat() {
              const lists = document.querySelectorAll('.b3-typography ul:not([data-replaced="true"]), .b3-typography ol:not([data-replaced="true"])');
              lists.forEach(list => {
                  const items = Array.from(list.children);
                  items.forEach(item => {
                      const nestedList = item.querySelector('ul, ol');
                      if (nestedList) {
                          item.parentNode.insertBefore(nestedList, item.nextSibling);
                      }
                  });
                  list.setAttribute('data-replaced', 'true');
              });
          }
      
          // Function to convert math blocks and inline math
          async function convertMathToWechat() {
              await loadMathJax();
              await Promise.all([renderMathBlocks(), renderInlineMath()]);
          }
      
          // Function to render math blocks
          async function renderMathBlocks() {
              const mathBlocks = document.querySelectorAll('.b3-typography div[data-subtype="math"]:not([data-replaced="true"])');
              const renderBlock = createRenderer(true);
              return Promise.all(Array.from(mathBlocks).map(async (block) => {
                  const mathContent = block.getAttribute('data-content');
                  try {
                      block.innerHTML = renderBlock(mathContent);
                      block.setAttribute('data-replaced', 'true');
                  } catch (error) {
                      console.error('Error rendering MathJax block:', error);
                  }
              }));
          }
      
          // Function to render inline math
          async function renderInlineMath() {
              const inlineMaths = document.querySelectorAll('span[data-type="inline-math"]:not([data-replaced="true"])');
              const renderInline = createRenderer(false);
              return Promise.all(Array.from(inlineMaths).map(async (span) => {
                  const mathContent = span.getAttribute('data-content');
                  try {
                      span.innerHTML = renderInline(mathContent);
                      span.style.verticalAlign = 'middle';
                      span.style.lineHeight = '1';
                      span.setAttribute('data-replaced', 'true');
                  } catch (error) {
                      console.error('Error rendering MathJax inline:', error);
                  }
              }));
          }
      
          // Function to create MathJax renderer
          function createRenderer(display) {
              return (mathContent) => {
                  const cleanedContent = mathContent.replace(/\displaystyle\s*{([^}]*)}/g, '$1');
                  window.MathJax.texReset();
                  const mjxContainer = window.MathJax.tex2svg(cleanedContent, { display });
                  const svg = mjxContainer.firstChild;
                  const width = svg.style[`min-width`] || svg.getAttribute(`width`);
                  svg.removeAttribute(`width`);
                  svg.style = `max-width: 70vw !important;`;
                  svg.style.width = width;
                  svg.style.display = `initial`;
                  if (display) {
                      return `<section style="box-sizing: border-box; border-width: 0px; border-style: solid; border-color: hsl(var(--border)); user-select: text !important; color: rgb(10, 10, 10); font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; text-align: center; overflow: auto;">${svg.outerHTML}</section>`;
                  }
                  return svg.outerHTML;
              };
          }
      
          // Function to load MathJax
          function loadMathJax() {
              return new Promise((resolve) => {
                  if (window.MathJax) {
                      resolve();
                      return;
                  }
                  window.MathJax = {
                      tex: {
                          inlineMath: [
                              ['$', '$'],
                              ['\(', '\)']
                          ]
                      },
                      svg: {
                          fontCache: 'none'
                      }
                  };
      
                  const script = document.createElement('script');
                  script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';
                  script.async = true;
                  script.onload = resolve;
                  document.head.appendChild(script);
              });
          }
      
          // Function to perform a synchronous POST request
          async function 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 (e) {
                  console.log(e);
                  return returnType === 'json' ? { code: e.code || 1, msg: e.message || "", data: null } : "";
              }
          }
      
          // 定义观察DOM变化的函数
          function 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;
          }
      })();
      

      PixPin_2024-11-14_00-43-07

  • 思源笔记

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

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

    22339 引用 • 89388 回帖
  • 代码片段

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

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

    69 引用 • 372 回帖
6 操作
Achuan-2 在 2024-11-16 21:18:00 更新了该帖
Achuan-2 在 2024-11-15 08:42:01 更新了该帖
Achuan-2 在 2024-11-14 11:23:25 更新了该帖
Achuan-2 在 2024-11-14 11:22:23 更新了该帖 Achuan-2 在 2024-11-14 11:16:02 更新了该帖 Achuan-2 在 2024-11-14 11:15:25 更新了该帖

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
Achuan-2
给时间以生命而不是给生命以时间,如果你喜欢我的分享,欢迎给我买杯咖啡 https://www.yuque.com/achuan-2 上海