Hexo插件:Obsidian标注语法兼容


> [!info] 这是标注的标题
> 这是一个标注块。
> 它支持 **Markdown**、[[内部链接|内部链接]] 和 [[插入文件|嵌入]]!
> ![[Engelbart.jpg]]

然而,当这些标注内容发布到Hexo博客时,无法正常显示。虽然Hexo本身支持类似的标注语法:

{% note class_name %} Content (md partial supported) {% endnote %}

但为了保持Obsidian的编辑体验,我们需要一个插件来解决这个问题。

解决方案

在Hexo博客的scripts目录下创建hexo-obsidian-tabs.js文件,并将以下内容复制到文件中:

'use strict';

/**
 * Hexo Obsidian Tabs Plugin
 * Support Obsidian Tabs plugin syntax rendering
 */

const { marked } = require('marked');

// Match the HTML output of tabs code blocks after markdown rendering
// Hexo converts ```tabs to <pre><code class="language-tabs">...</code></pre>
const TABS_HTML_REGEX = /<pre[^>]*>\s*<code[^>]*class="[^"]*language-tabs[^"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/g;

// Match tab header in HTML content
const TAB_HEADER_REGEX = /tab:\s*([^\n]+)/;

// Match code blocks within tab content
const CODE_BLOCK_REGEX = /&#96;&#96;&#96;(\w*)\n([\s\S]*?)&#96;&#96;&#96;/g;

function parseTabsHtml(content) {
  return content.replace(TABS_HTML_REGEX, (match, codeContent) => {
    // Decode HTML entities
    let decodedContent = codeContent
      .replace(/&#x2F;/g, '/')
      .replace(/&#x27;/g, "'")
      .replace(/&lt;/g, '<')
      .replace(/&gt;/g, '>')
      .replace(/&quot;/g, '"')
      .replace(/&amp;/g, '&');

    // First, restore code blocks that were encoded
    // Convert &#96; back to ` for code blocks
    decodedContent = decodedContent.replace(/&#96;/g, '`');

    // Split by tab headers
    const lines = decodedContent.split('\n');
    const tabs = [];
    let currentTab = null;
    let currentContent = [];

    for (const line of lines) {
      const tabMatch = line.match(/^tab:\s*(.+)$/);

      if (tabMatch) {
        // Save previous tab
        if (currentTab) {
          tabs.push({
            title: currentTab,
            content: currentContent.join('\n').trim()
          });
        }
        // Start new tab
        currentTab = tabMatch[1].trim();
        currentContent = [];
      } else if (currentTab) {
        // Collect current tab content
        currentContent.push(line);
      }
    }

    // Save last tab
    if (currentTab) {
      tabs.push({
        title: currentTab,
        content: currentContent.join('\n').trim()
      });
    }

    if (tabs.length === 0) {
      return match;
    }

    // Generate unique ID
    const tabsId = 'obsidian-tabs-' + Math.random().toString(36).substr(2, 9);

    // Build HTML
    let html = `<div class="obsidian-tabs" id="${tabsId}">`;

    // Tab buttons
    html += '<div class="obsidian-tabs-nav">';
    tabs.forEach((tab, index) => {
      const activeClass = index === 0 ? ' active' : '';
      html += `<button class="obsidian-tab-btn${activeClass}" data-tab="${tabsId}-tab-${index}">${escapeHtml(tab.title)}</button>`;
    });
    html += '</div>';

    // Tab content - render markdown for each tab
    html += '<div class="obsidian-tabs-content">';
    tabs.forEach((tab, index) => {
      const activeClass = index === 0 ? ' active' : '';
      // Use hexo's markdown renderer
      const renderedContent = hexo.render.renderSync({ text: tab.content, engine: 'markdown' });
      html += `<div class="obsidian-tab-panel${activeClass}" id="${tabsId}-tab-${index}">${renderedContent}</div>`;
    });
    html += '</div>';

    html += '</div>';

    return html;
  });
}

function escapeHtml(text) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

// Register filter - process after markdown rendering
hexo.extend.filter.register('after_post_render', function(data) {
  if (!data.content) return data;
  data.content = parseTabsHtml(data.content);
  return data;
}, 1); // Lower priority to run after markdown rendering

// Inject CSS
hexo.extend.injector.register('head_end', `
<style>
.obsidian-tabs {
  margin: 1.5em 0;
  border: 1px solid var(--tab-border-color, #e0e0e0);
  border-radius: 8px;
  overflow: hidden;
  background: var(--tab-bg, #fafafa);
}

.obsidian-tabs-nav {
  display: flex;
  background: var(--tab-nav-bg, #f0f0f0);
  border-bottom: 1px solid var(--tab-border-color, #e0e0e0);
  overflow-x: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.obsidian-tabs-nav::-webkit-scrollbar {
  display: none;
}

.obsidian-tab-btn {
  padding: 12px 20px;
  border: none;
  background: transparent;
  color: var(--tab-text-color, #666);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.obsidian-tab-btn:hover {
  color: var(--tab-hover-color, #333);
  background: var(--tab-hover-bg, rgba(0,0,0,0.03));
}

.obsidian-tab-btn.active {
  color: var(--tab-active-color, #448aff);
  border-bottom-color: var(--tab-active-color, #448aff);
  background: var(--tab-bg, #fafafa);
}

.obsidian-tabs-content {
  padding: 20px;
  background: var(--tab-bg, #fafafa);
}

.obsidian-tab-panel {
  display: none;
  animation: fadeIn 0.3s ease;
}

.obsidian-tab-panel.active {
  display: block;
}

.obsidian-tab-panel > *:first-child {
  margin-top: 0;
}

.obsidian-tab-panel > *:last-child {
  margin-bottom: 0;
}

.obsidian-tab-panel pre {
  margin: 1em 0;
  border-radius: 6px;
}

.obsidian-tab-panel code {
  font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}

/* Fix code block background color inside tabs */
.obsidian-tab-panel pre code {
  background: transparent !important;
}

.obsidian-tab-panel .highlight {
  background: transparent !important;
}

.obsidian-tab-panel .highlight pre {
  background: var(--code-bg, #f5f5f5) !important;
}

.obsidian-tab-panel figure.highlight {
  background: var(--code-bg, #f5f5f5) !important;
}

.obsidian-tab-panel td.gutter {
  background: var(--code-gutter-bg, #f0f0f0) !important;
}

.obsidian-tab-panel td.code {
  background: var(--code-bg, #f5f5f5) !important;
}

@media (prefers-color-scheme: dark) {
  .obsidian-tab-panel .highlight pre,
  .obsidian-tab-panel figure.highlight {
    --code-bg: #2d2d2d;
    --code-gutter-bg: #252525;
  }
}

@media (prefers-color-scheme: dark) {
  .obsidian-tabs {
    --tab-bg: #1e1e1e;
    --tab-nav-bg: #252526;
    --tab-border-color: #333;
    --tab-text-color: #999;
    --tab-hover-color: #ccc;
    --tab-hover-bg: rgba(255,255,255,0.05);
    --tab-active-color: #58a6ff;
  }
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(5px); }
  to { opacity: 1; transform: translateY(0); }
}
</style>
`);

// Inject JavaScript
hexo.extend.injector.register('body_end', `
<script>
(function() {
  document.querySelectorAll('.obsidian-tab-btn').forEach(function(btn) {
    btn.addEventListener('click', function() {
      var tabsContainer = this.closest('.obsidian-tabs');
      var tabId = this.getAttribute('data-tab');

      tabsContainer.querySelectorAll('.obsidian-tab-btn').forEach(function(b) {
        b.classList.remove('active');
      });
      tabsContainer.querySelectorAll('.obsidian-tab-panel').forEach(function(p) {
        p.classList.remove('active');
      });

      this.classList.add('active');
      document.getElementById(tabId).classList.add('active');
    });
  });
})();
</script>
`);

console.log('[hexo-obsidian-tabs] Plugin loaded successfully!');

使用说明

  1. 将上述代码保存为hexo-obsidian-tabs.js文件
  2. 将该文件放置在Hexo博客的scripts目录下
  3. 重启Hexo服务器,插件将自动加载

该插件会自动处理Obsidian标注语法,并将其转换为Hexo可识别的格式,确保内容在博客上正确显示。


文章作者: gloamfox
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 gloamfox !
 上一篇
Hexo插件:Obsidian tabs语法兼容
本文介绍了如何创建一个Hexo插件,解决Obsidian中的tabs语法在Hexo平台上无法正常显示的问题。作者详细提供了完整的插件代码,包括HTML解析、CSS样式和JavaScript交互功能,使Obsidian的tabs组件能够在Hexo博客中正确渲染和展示。
2026-04-07
下一篇 
npm国内镜像源配置
本文介绍了npm在国内下载速度慢的问题,并提供了解决方案。
2026-04-04
  目录