Anki默认不支持Markdown语法,本文forkanki-md-template实现了Anki牌组对Markdown
、Mermaid
、MathJax
,且支持双端渲染。
移动端的MathJax解析存在一定的语法解析问题,可以通过[工具发布]LLM输出内容所含公式一键适配Anki卡牌 解决,但是适配安卓端后在Win端就无法正常解析,两者不相容,初步分析这是Ankidroid对Webview调用时的语法转义问题,暂时无法解决。
正面 中引用了本站自建CDN,可以按需替换。
正面
<div class="md-content">
{{Front}}
</div>
<script>
var getResources = [
getScript("https://blog.ckh-cn.site/markdown-it.min.js"),
getScript("https://blog.ckh-cn.site/highlight.min.js")
];
var mermaidModule = null;
// 加载资源并初始化
Promise.all(getResources)
.then(() => {
// 动态导入 mermaid 模块
return loadMermaidModule();
})
.then(() => {
parseMarkDownFn();
})
.catch(error => {
consoleLog('Failed to load required resources: ' + error);
});
// 动态加载 mermaid 模块
async function loadMermaidModule() {
try {
// 使用 import() 动态导入 mermaid
const mermaidImport = await import('https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs');
mermaidModule = mermaidImport.default;
// 初始化 mermaid
mermaidModule.initialize({
startOnLoad: false,
theme: 'default',
flowchart: {
useMaxWidth: true
},
securityLevel: 'loose'
});
consoleLog('Mermaid module loaded and initialized successfully');
} catch (error) {
consoleLog('Failed to load mermaid module: ' + error.message);
mermaidModule = null;
}
}
function getScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.onload = () => {
consoleLog('Script loaded: ' + url);
resolve();
};
script.onerror = () => reject(new Error(`Failed to load script from ${url}`));
script.src = url;
document.head.appendChild(script);
});
}
// 在anki中通过窗口查看调试信息 没控制台啊
function consoleLog(str) {
return;
var div = document.createElement('div');
div.innerHTML = str;
div.style.color = '#0066cc';
div.style.fontFamily = 'monospace';
div.style.fontSize = '12px';
div.style.padding = '2px 0';
document.body.appendChild(div);
}
// 清除br标签
var clearBR = (str) => {
str = str.replace(/<br>/g, '\r\n');
return str;
}
// 反转义 HTML 实体
var unescapeHTMLEntities = (innerHTML) =>
Object.assign(document.createElement('textarea'), { innerHTML }).value;
// 解析 mermaid 图表
async function renderMermaidDiagrams() {
// 检查 mermaid 模块是否可用
if (!mermaidModule) {
consoleLog('Mermaid module not available, skipping diagram rendering');
return;
}
try {
// 查找所有 mermaid 代码块
const mermaidBlocks = document.querySelectorAll('pre.mermaid-code-block code.language-mermaid, code.language-mermaid');
consoleLog(`Found ${mermaidBlocks.length} mermaid blocks`);
let mermaidIndex = 0;
for (const element of mermaidBlocks) {
try {
const code = element.textContent || element.innerText;
if (!code || code.trim().length === 0) {
consoleLog('Empty mermaid code block, skipping');
continue;
}
consoleLog('Rendering mermaid: ' + code.substring(0, 100) + '...');
const graphDefinition = code.trim();
const { svg, bindFunctions } = await mermaidModule.render(
`mermaid-diagram-${mermaidIndex++}`,
graphDefinition
);
// 创建新的 div 来替换原有的代码块
const mermaidDiv = document.createElement('div');
mermaidDiv.className = 'mermaid-diagram';
mermaidDiv.innerHTML = svg;
mermaidDiv.style.margin = '10px 0';
mermaidDiv.style.textAlign = 'center';
// 替换原有的代码块
element.closest('pre') ?
element.closest('pre').replaceWith(mermaidDiv) :
element.parentNode.replaceWith(mermaidDiv);
consoleLog('Mermaid diagram rendered successfully');
} catch (error) {
consoleLog('Mermaid rendering error: ' + error.message);
}
}
} catch (error) {
consoleLog('General mermaid processing error: ' + error.message);
}
}
// 解析(入口main方法)
var parseMarkDownFn = () => {
const md = markdownit({
html: true,
linkify: true,
typographer: true,
breaks: true,
highlight: function (str, lang) {
if (lang && hljs && hljs.getLanguage && hljs.getLanguage(lang)) {
try {
// 特殊处理 mermaid 代码块
if (lang === 'mermaid') {
return `<pre class="mermaid-code-block" style="background: #f8f8f8; padding: 10px; border-radius: 4px;"><code class="language-mermaid">${str}</code></pre>`;
}
return hljs.highlight(str, { language: lang }).value;
} catch (__) {
consoleLog('Highlight.js error: ' + __.message);
}
}
// 特殊处理 mermaid 代码块(即使 hljs 不可用)
if (lang === 'mermaid') {
return `<pre class="mermaid-code-block" style="background: #f8f8f8; padding: 10px; border-radius: 4px;"><code class="language-mermaid">${str}</code></pre>`;
}
return '';
},
});
document.querySelectorAll('.md-content').forEach((div, index) => {
consoleLog('Processing container element ' + index);
var text = unescapeHTMLEntities(div.innerHTML).trim();
consoleLog('Markdown text length: ' + text.length);
text = clearBR(text);
var html = md.render(text);
var newDiv = document.createElement('div');
var hr = document.createElement('hr');
newDiv.innerHTML = html;
newDiv.className = 'markdown-body';
div.parentNode.insertBefore(newDiv, div.nextSibling);
index === 1 ? div.parentNode.insertBefore(hr, div.nextSibling) : null;
div.className = `x-${index}`;
div.style.display = 'none';
});
// 渲染 mermaid 图表
setTimeout(() => {
renderMermaidDiagrams().catch(error => {
consoleLog('Error in renderMermaidDiagrams: ' + error.message);
});
}, 300);
}
</script>
背面
{{FrontSide}}
<div class="md-content">{{Back}}</div>
CSS
@import url('https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown.min.css');
@import url('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/base16/onedark.min.css');
@import url('https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css');
/*
把这里的样式代码 ctrl a 复制到卡片模板 的样式区域
*/
/* 初始状态下设置透明度为0 */
.md-content {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
/* 卡片内容展示样式 */
.markdown-body {
box-sizing: border-box;
min-width: 200px;
margin: 0 auto;
padding: 16px;
}