思源笔记发布到微信公众号,对于公式、链接、多级列表会有一些问题,终于忍无可忍,让 GPT 给我写了处理的代码,现在这三种样式都能很好地粘贴到微信公众号啦。再加上我开发的 Tsundoku 主题对发布到微信公众号做了一点样式处理,现在基本上,思源笔记是什么样式,发到微信公众号也是什么样式了,舒服!以后就可以专心写公众号啦!不用每次发布,都折腾排版问题、样式缺失问题。
完整 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);
}
}
});
// 选择所有的 div.b3-typography 元素
const typographyDivs = document.querySelectorAll('div.b3-typography');
typographyDivs.forEach(typographyDiv => {
// 获取最后一个子元素
const lastChild = typographyDiv.lastElementChild;
// 检查最后一个子元素是否是 div[data-subtype="math"]
if (lastChild && lastChild.matches('div[data-subtype="math"]')) {
// 创建新的 <p> 元素
const newParagraph = document.createElement('p');
newParagraph.innerHTML = '​'; // 插入零宽度空格
// 将新的 <p> 元素添加到 div.b3-typography 中
typographyDiv.appendChild(newParagraph);
}
});
}
// 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;
}
})();
具体原理解释
数学公式处理
处理方法
思源笔记的公式不能直接复制粘贴到微信公众号,于是将公式替换为 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}
行间公式:麦克斯韦方程组
修改前,发到公众号的样式
修改后,发到公众号的样式
处理链接
处理方法
写 js 代码,处理思源笔记预览模式的超链接,以发送到微信公众号
- 微信公众号的外链(除了 https://mp.weixin.qq.com/.*以外的链接)是不能直接跳转到,所以要把思源笔记的超链接都变为普通文本,然后链接放在锚文本的后面,即变为
[锚文本](链接)
的形式 - 如果外链锚文本本身就是链接,就不需要用
[text]({href})
样式 - 对转换后的链接添加
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);
}
});
}
处理后的结果
链接样式举例
- 链接 1,处理为 markdown 源码样式:导出功能添加「是否将链接导出为脚注」 · Issue #11169 · siyuan-note/siyuan
- 链接 2,本身就为锚文本,不进行转换:https://github.com/siyuan-note/siyuan/issues/11169
- 公众号链接,粘贴到微信公众号可以保留跳转链接,不进行处理:MOC:面向主题地去管理笔记链接
修改前,发到公众号的样式(注意外链在微信公众号编辑器是有颜色的,但是一旦预览和发布,就会变为普通文本)
修改后,发到公众号的样式
处理多级列表
处理方法
思源笔记目前的渲染是,次级的列表会包含在 <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 结构进行重新调整,包括
- 和
-
多巴胺
-
基本概念
- 多巴胺是一种神经递质,属于儿茶酚胺和苯乙胺家族。
- 它在大脑和身体中扮演着多种角色,尤其是在奖励、动机和愉悦感的调节中。
- 化学式为C_8H_{11}NO_2,其结构中包含一个苯环和一个胺基。
-
生理功能
-
奖励与动机
- 多巴胺在大脑的奖励系统中起关键作用,影响动机和愉悦感。
- 它的释放与愉快的体验和行为强化有关。
-
运动控制
- 在中脑的黑质区,多巴胺调节运动功能。
- 多巴胺缺乏与帕金森病有关,导致运动障碍。
-
情绪与认知
- 多巴胺影响情绪调节和认知功能。
- 其失衡可能与精神疾病如抑郁症和精神分裂症有关。
-
-
合成与代谢
-
合成
- 多巴胺由氨基酸酪氨酸通过一系列酶促反应合成。
- 关键步骤包括酪氨酸羟化酶催化生成左旋多巴(L-DOPA),然后由多巴脱羧酶转化为多巴胺。
-
代谢
- 多巴胺通过单胺氧化酶(MAO)和儿茶酚-O-甲基转移酶(COMT)代谢。
- 代谢产物包括高香草酸(HVA),可通过尿液排出。
-
-
临床相关性
-
帕金森病
- 由于黑质区多巴胺神经元的退化,导致运动功能障碍。
- 治疗通常涉及多巴胺替代疗法,如左旋多巴。
-
精神疾病
- 精神分裂症与多巴胺系统的过度活跃有关。
- 抗精神病药物通常通过阻断多巴胺受体来发挥作用。
-
成瘾
- 多巴胺在成瘾行为中起重要作用,许多药物通过增加多巴胺释放或阻止其再摄取来产生愉悦感。
- 成瘾治疗可能涉及调节多巴胺系统的药物。
-
-
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();
处理后的结果
多级列表举例
修改前,发布到微信公众号的样式
修改后,发到公众号的样式
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于