支持Markdown的Anki模版

Anki默认不支持Markdown语法,本文forkanki-md-template实现了Anki牌组对MarkdownMermaidMathJax,且支持双端渲染。

移动端的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;
}

支持Markdown的Anki模版

https://blog.ckh-cn.site/index.php/2025/08/31/176.html

作者

CKH

发布时间

2025-08-31

许可协议

CC BY 4.0

OS: Linux 7a6cc33c7bf6 5.15.0-113-generic #123-Ubuntu SMP Mon Jun 10 08:16:17 UTC 2024 x86_64
CPU Info:
Memory Info:
评论