Hexo插件:Obsidian tabs语法兼容


在Obsidian中,我使用了tabs插件来显示多个页签的切换功能,这种展示方式非常直观且实用。然而,这种语法并非Markdown原生支持,因此当文章发布到Hexo平台后,无法正常展示。

以下是一个示例的tabs语法:

规定月费,适合高频使用者
首次启动时浏览器弹窗登录
推荐给日常开发使用的用户

用多少付多少
在Anthropic Console获取Key

为了解决这个问题,我编写了一个Hexo插件。首先,在Hexo目录下创建一个名为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!');


文章作者: gloamfox
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 gloamfox !
 上一篇
Claude Code通关手册(二):搞清权限,效率翻倍
本文详细介绍了Claude Code的权限系统配置方法,包括五种内置权限模式、项目自定义权限配置、安全最佳实践以及全局配置模板。通过合理设置权限,开发者可以在保障安全的同时减少烦人的权限提示,提升工作效率。
2026-04-07
下一篇 
Hexo插件:Obsidian标注语法兼容
文章介绍了如何解决Obsidian标注(callout)语法在Hexo博客上无法正常显示的问题。作者创建了一个名为'Hexo Obsidian Tabs'的插件,该插件能将Obsidian的标注语法转换为Hexo可识别的格式。文章详细提供了插件的完整代码、安装步骤和使用说明,确保内容在博客上正确显示,同时保持Obsidian的编辑体验。
2026-04-07
  目录